Е. Буткевич Пишем программы и игры


Глава 6 Организация многопоточных приложений



страница6/15
Дата22.06.2019
Размер2.79 Mb.
ТипКраткое содержание
1   2   3   4   5   6   7   8   9   ...   15
Глава 6

Организация многопоточных приложений

В прошлой главе мы реализовали основную часть игры «Змейка», придержива­ясь классических принципов игры. Используя стандартное управление, можно ползать по всему экрану и собирать сердечки, удлиняя змею. При столкновении змеи со стеной или с собственным телом игра заканчивается. Пока что главное отличие от классической игры заключается в том, что змейка стоит на месте, если не нажимать управляющие клавиши. Таким образом, эта увлекательная и разви­вающая игра теряет всякий смысл.

Основная проблема в реализации самостоятельного продвижения змеи такова, что если программно зациклить какое-то действие, то приложение не будет иметь возможности выйти из цикла и обработать нажатие функциональных клавиш. На помощь в решении этой проблемы приходит многозадачность мобильной опера­ционной системы.

Многозадачность

В современных операционных системах, в том числе и мобильных, реализовано понятие многозадачности, которое заключается в одновременном выполнении не­скольких программ. Слово «одновременно» здесь надо понимать так: каждой из программ поочередно отводится определенное количество процессорного време­ни, в течение которого программа выполняется, а затем снимается с процессора, уступая свое место другой программе.

Сама программа, в нашем случае — мидлет, также может создавать несколько па­раллельных процессов, которые будут выполняться одновременно. Такие процес­сы в оригинале языка Java называются thread. Понятие thread в русской литерату­ре переводят по-разному. В буквальном переводе термин обозначает «нить», но мы все же не швеи-мотористки, а какие-никакие программисты. Часто thread пе­реводят как «поток», но, к сожалению, этот термин уже занят потоками ввода-вывода (streams), на которых мы остановимся позже. Так что в данной главе мы будем пользоваться транслитерацией термина — «тред».

В любом мидлете существует, по крайней мере, хотя бы один тред — главный тред выполнения мидлета, из которого можно создать другие треды для одновремен­ного выполнения. В языке Java треды представлены объектами класса Thread.

Класс Thread 67

Класс Thread

Класс Thread реализует интерфейс Runnable, который содержит объявление лишь одного метода: void run(). В методе run () должны быть реализованы все действия, предусмотренные для выполнения в конкретном треде. Сам класс Thread содер­жит пустую реализацию метода run(), поэтому задать действия для создаваемого треда можно следующими двумя способами:



  • расширить класс Thread и переопределить метод гип();

  • реализовать интерфейс Runnable.

Мы будем пользоваться вторым способом, поскольку в наших примерах класс, создающий тред, уже наследован от какого-то другого класса, а множественное наследование в языке Java не поддерживается. Для каждого способа класс Thread содержит отдельный конструктор:

  • ThreadО — конструктор, не принимающий аргументов; вызывается из конст­
    руктора класса-потомка, расширяющего класс Thread;

  • Thread(Runnable target) — конструктор, принимающий в качестве аргумента
    объект, реализующий интерфейс Runnabl e;

После того как объект треда создан, предписанные в методе run О действия еще не начинают выполняться. Для управления тредом существует несколько методов класса Thread:

  • void start() — начать выполнение треда. Виртуальная машина вызывает метод
    run (), который никогда не вызывается программистом явно. После вызова ме­
    тода start О начинается параллельное выполнение программы в двух направ­
    лениях: тред идет своей дорогой, а поезд идет своей;

  • void sieepClong mill is) — приостанавливает выполнение треда на mil lis мил­
    лисекунд.

В этом месте мы остановимся и попробуем применить изученную теорию на прак­тике. Вернемся к программе фотоальбома, который мы реализовали во второй гла­ве. Усовершенствуем эту программу таким образом: добавим показ слайд-шоу, то есть автоматическую смену картинок на экране.

Организуем управление автоматической сменой картинок в отдельном треде. Для этого в основном классе нашего фотоальбома SlideShow реализуем интерфейс Runnable. Теперь декларация класса будет выглядеть следующим образом:

public class SlideShow extends MIDlet implements CommandListener. Runnable Следует также добавить в класс SI ideShow новое поле для объекта треда: private Thread thread;

Как уже было сказано ранее, все действия, выполняемые тредом, следует реали­зовать в методе гип ():

public void runО { // вечный цикл while (true) { try {

// удалить из формы текущую картинку

68 Глава 6. Организация многопоточных приложений

form.delete(O):



// добавить в форму новую картинку

// увеличиваем текущий номер картинки и берем

// остаток от деления на общее число картинок

setlmage("/"+((siideNum++)£maxSlideNum+1)+".png");

// остановить выполнение треда на 3 секунды

thread.sleep(3000):

// обработать исключительную ситуацию

} catch (InterruptedException e) {

В методе startAppC) осталось создать объект треда и дать команду для начала его

выполнения:

// создать объект треда

thread = new Thread(this);

// начать выполнение треда

thread.startO;

Заметим, что после запуска слайд-шоу остановить смену картинок будет очень проблематично, поэтому стоит все же воздержаться от бесконечных циклов и за­вести флаг, сбрасывая который, можно организовать завершение работы треда.



Приоритеты тредов

Как уже было отмечено, одновременное выполнение нескольких тредов доста­точно условно, поскольку физически в каждый момент времени выполняется толь­ко один тред. Переключение тредов и количество выделенного процессорного времени зависит от операционной системы, однако программист тоже может по­влиять на этот процесс. Иногда некоторым тредам требуется выделить большее или меньшее количество процессорного времени. Технология мультитрединга позволяет определить приоритеты выполнения для каждого треда.

Приоритеты тредов представлены целыми числами в диапазоне от 1 до 10. Чем выше значение приоритета, тем больше времени выделяется треду операционной системой. Также в классе Thread определены константы MIN_PRIORITY, NORM_PRIORITY и MAX_PRIORITY, представляющие значения минимального, нормального и макси­мального приоритетов соответственно.

Работас приоритетами осуществляется с помощью следующих методов класса Thread:

■ void setPrioritydnt newPriority) — установить новый приоритет треду. При
попытке выставить приоритет, выходящий за допустимый диапазон, будет
сформировано исключение IllegalArgumentException;

■ int getPriorityC) — возвращает текущий приоритет треда.


Продемонстрируем на небольшом примере многозадачность системы. Запустим
параллельно два треда, каждый из которых в цикле будет выводить сообщения

Приоритеты тредов

69

в системную область (рис. 6.1). Главный тред будет выполнять основной код мид-лета в методе startApp(), а для второго треда создадим отдельный класс SecondThread: public void startAppO {

// создать второй тред

SecondThread thread = new SecondThread O;

// запустить второй тред

thread.startO):

// в цикле выводить сообщения в системную область

for(int 1-1; i<=5; i++)

System.out.printlnC'Main Thread: "+i):

private class SecondThread extends Thread {

// в цикле выводить сообщения в системную область public void run() {

fordnt 1-1: i<=5: 1++)

System.out.println("Second Thread: "+i): )



ISJ2MEWireless Toolkit - SlideShm»




:.т*:.Ш:.:РгЬ&&.-;№$^--■■-.■;:.:■.■"';.ixi JU^"




;! ;$^г&й»:Ргфе1:;,;1 ^ Open Project, ■„, j|...^S«Hin0s

1 В Bufcl i % 1

j . bevte^JDelautGreyPhone

ll ■'■■ ■■' .:.■

Main Thread: 1




Second Thread: 1




Main Thread: 2




Second Thread: 2




Main Thread: 3




Second Thread: 3




Main Thread: 4




Second Thread: 4




Main Thread: 5




Second Thread: 5










Рис. 6.1. Результаты параллельной работы двух тредов с одинаковым приоритетом

На рисунке мы видим, что переключение тредов работает, как швейцарские часы, и сообщения от каждого треда выдаются по очереди, несмотря на то, что вывод сообщений в программе представлен единым непрерывным циклом.

Попробуем теперь перед запуском второго треда выставить ему повышенный при­оритет thread.setPriority(Thread.MAX_PRIORITY) и посмотрим, что же из этого по­лучится (рис. 6.2).

Видим, что картина существенно поменялась: главный тред не начал выполнять­ся, пока тред с повышенным приоритетом не закончил работу. Не исключена си-



70

Глава 6. Организация многопоточных приложений

туация, что при наличии множества тредов с высоким приоритетом какой-то низ­коприоритетный тред вообще не получит управления. Такую ситуацию можно предотвратить с помощью следующих методов класса Thread:

  • void yield() — принудительное переключение системы на выполнение следу­
    ющего треда;

  • void joinC) — ожидание завершения работы треда; останавливает работу про­
    граммы, из которой был вызван данный тред.

•ШгМЕ Wirelcw Tiraikit -




•j: .$a New Project-;.,. j $£f OpenProject;, .,,■{[' ^ S

mgs,... SJ'-ewW :%Run-



j;. Device: JDefaultGtayPhone

Second Thread: Second Thread: Second Thread: Second Thread: Second Thread: Kain Thread: 1 Main Thread: Ham Thread: Main Thread: Main Thread:

Д|^ р, ■■ ■;;:к j ,,'-;:., ?.''

:;^'-riц fЯsЪy~1 -iy'



Рис. 6.2. Результаты параллельной работы двух тредов с разным приоритетом

При создании тред получает приоритет создавшего его треда. Как правило, все треды работают с нормальным (NORMPRIORITY) приоритетом. Треды, ожидающие какого-то события, например нажатия кнопки, могут получить повышенный приоритет, а тре­ды, выполняющие длительную, ресурсоемкую работу — пониженный приоритет.

Отследить состояние системы с точки зрения выполняющихся в конкретный мо­мент тредов можно с помощью следующих методов:

boolean isAiiveO — возвращает значение true, если тред был запущен и до сих


пор не прекратил выполнение;

• Thread currentThreadC) — возвращает тред, выполняемый в данный момент вре­мени;

■ intactiveCountС) — возвращает количество тредов, выполняемых системой в дан­
ный момент времени.

Синхронизация тредов

Основная сложность при работе с тредами заключается в том, что разные треды, по­рожденные одним мидлетом, имеют общее адресное пространство, то есть одни и те же данные могут быть доступны из разных тредов. В чем здесь криминал? Рассмот­рим, например, ситуацию, когда два треда работают с одним и тем же вектором. Пер­вый тред получает длину вектора, затем происходит смена выполняемого треда, и второй тред удаляет элементы из этого же вектора. Первый тред возобновляет ра-



Синхронизация тредов

71


»ЩН0И.72МИЖ тЩ\










Г Е1







И SUdeSho w ■"

java/lang/ArraylndexOut OfBoundsException

Back


^ * 1 i.:

!;







боту и с удивлением обнаруживает, что получен­ная длина вектора не соответствует действитель­ности, формирует исключение неверного индекса и падает с таким вот грохотом (рис. 6.3).

Рис. 6.3. Демонстрация ошибки при синхронизации

Это далеко не единственный пример. Контек­стное переключение тредов может произойти в любой момент. В самой простейшей операции между вычислением и присваиванием резуль­тата может вклиниться другой тред и поменять данные. Результаты такой путаницы могут быть совершенно непредсказуемы. Описанная проблема разрешается в языке Java при помощи технологии синхронизации тредов. Идея заключается в блокировке тредом доступа к объекту на время работы с общими данными. Перед тем как начать работать с данными, тред ставит блок на доступ к объекту, и все осталь­ные треды при необходимости работы с этими данными ожидают снятия блока. Блокировка осуществляется с помощью опера­тора synchronized (Object obj) {...}. В фигурных скобках размещается критическая секция про­граммы — код приложения, требующий синхро­низации. В качестве аргумента obj оператору пе­редается объект, который будет заблокирован. Если в момент выполнения оператора synchro­nized синхронизируемый объект уже заблокиро­ван каким-то другим тредом, то выполнение при­остановится до тех пор, пока блок не будет снят. Если какой-то метод требует синхронизации всех своих операторов, то весь метод может быть помечен как synchronized, например:

public synchronized void someActionWithDataO {...}

В таком случае блокируется объект this, вызвавший данный метод. Блок снима­ется только после завершения работы метода.

Очень неприятная ситуация может возникнуть при перекрестной блокировке, когда один тред получил управление синхронизированным объектом А, а второй — синх­ронизированным объектом В. После этого первый тред пытается вызвать синхрони­зированный метод объекта В, а второй — аналогичный метод объекта А. Оба треда застрянут и будут ждать освобождения объектов, блокированных друг другом, а про­грамма с такой структурой тредов просто зависнет. В теории такая ситуация называ­ется взаимоблокировкой (deadlock). Так что при разработке приложений следует очень внимательно отнестись к вопросам корректной реализации синхронизации.

В языке Java существует способ предотвращения взаимоблокировки. Внутри син­хронизированной критической секции можно приостановить работу треда с по­следующим ее возобновлением. Класс Object содержит несколько методов wait, которые откладывают выполнение треда. Отличие этих методов от метода sleep


72 Глава 6. Организация многопоточных приложений

класса Thread заключается в том, что во время приостановки работы треда блоки­ровка с синхронизируемого объекта снимается. Методы класса Object, отвеча­ющие за остановку и возобновление тредов:



  • voidwait() — текущий тред снимает блокировку с объектаи приостанавлива­
    ется до тех пор, пока из какого-либо другого треда не будут вызваны методы
    notify О илиnotifyAlK);

  • void wait(long timeout) — текущий тред снимает блокировку и откладывает
    выполнение, пока из какого-либо другого треда не будут вызваны методы
    noti fy () или noti fyAI 1() или не истечет тайм-аут длиной timeout миллисекунд;

  • voidwaitClongtimeout. intnanos) —аналогичен предыдущему методу, с точнос­
    тью тайм-аута до наносекунд, которая задается аргументом nanos. Вызов этих
    трех методов допускается только из синхронизированных методов или крити­
    ческих секций программы;

  • void notify() — возобновляет работу одного приостановленного треда, произ­
    вольно выбранного системой;

  • void noti fyAI 10 — возобновляет работу всех приостановленных на данном объекте
    тредов. Фактически начинает работу только один тред, первым восстановивший
    блокировку объекта, остальные ждут снятия блока в обычном режиме.

Следует заметить, что при обращении к общим данным синхронизация нужна только в том случае, если один из тредов изменяет данные. В случае, когда все треды просто читают данные, не изменяя их, синхронизация тредов не требуется.

Класс Snake

Теперь у нас достаточно информации, чтобы добавить в игру самостоятельное продвижение змейки. Для этого необходимо реализовать в классе Snake интер­фейс Runnabie, а в методе runО организовать цикл продвижения змеи в текущем направлении. Декларация класса Snake будет выглядеть теперь так: private class Snake extends Canvas implements Runnable

В методе run() будем проверять текущее направление движения змейки и в зави­симости от этого вызывать соответствующий метод продвижения. Обратим вни­мание, что метод runО и метод keyPressed(int keyCode) будут изменять данные од­ного и того же объекта, поэтому здесь потребуется синхронизация. В объявлении метода keyPressed добавим ключевое слово synchronized, а в методе run О выделим критическую секцию кода:

public void run() {

// пока не поднят флаг конца игры while (IgameOverFlag) {

// синхронизировать с управлением synchronized (this) {



It продвинуть змейку в текущем направлении if(direction==UP) {checkMove(xHead. yHead-7); moveUpO:} if(direction==LEFT) {checkMove(xHead-7, yHead): moveLeftO:} if(direction==RIGHT) {checkMove(xHead+7. yHead); moveRightO:} if(direction==DOWN) {checkMove(xHead, yHead+7): moveDownO:}

Класс Snake 73

// вызвать перерисовку экрана

repaintO:

try {

// приостановить тред на speed миллисекунд



Thread.sieep(I evel*speed) : } catch (InterruptedException e) {

Заметим, что в класс Snake мы ввели два новых поля, private byte level и private int speed, которые будут отвечать за скорость передвижения нашей змейки. Поле level содержит уровень сложности игры, а поле speed — коэффициент ускорения для вычисления времени, на которое «засыпает» тред перед очередным продвижением змейки. Чем меньше этот параметр, тем быстрее будет двигаться наша змейка. В конструкторе установим speed = 100, level =5, а в методе checkMove после каждого «съеденного» сердечка будем увеличивать скорость передвижения змейки: // проверить совпадение координат передвижения // с координатами сердца if ( хН — xHeart && уН — уHeart ) {

eatFlag = true;

// увеличить скорость

speed--; } else

eatFlag - false:

Все, что осталось сделать, это создать в методе startApp() новый тред и запустить его: // создать объект треда автоматического передвижения Thread moveThread = new Thread(curSnake); // начать выполнение треда moveThread.startO:

Компилируем, запускаем и наслаждаемся процессом! Играть стало интересней, играть стало веселее, а корпорация Nokia кусает локти от зависти. На достигну­том мы, конечно же, останавливаться не будем и в следующих главах еще усовер­шенствуем нашу игру. Добавим картинок-заставок, таблицу рекордов, возмож­ность выбора уровня игры и прочие прелести. Оставайтесь с нами, не переключайте канал, дальше будет еще интереснее!



* * *

В этой главе мы опробовали многозадачность виртуальной машины Java на не­хитрых примерах. Иногда без создания нового треда просто не обойтись( как это было в случае со змейкой. При создании большой полноценной программы сле­дует задуматься, какие из частей программы могут выполняться одновременно. Зачастую оправдано создание отдельных тредов для каждой подзадачи. После создания тредов следует подумать над распределением приоритетов. Особое вни­мание следует уделить синхронизации тредов, если они обращаются к одним и тем же данным программы.



Глава 7

Сохранение данных

и параметров приложения

Как показывает практика, люди делятся на две категории: на тех, кто прошел Doom от начала до конца, и на тех, у кого не хватило терпения, реакции или секретных кодов, чтобы выпустить кишки всем злобным монстрам, которыми щедро нашпи­гован каждый новый уровень. Поколение игры Doom хорошо знает основной прин­цип ЗО-стрелялок: «Главное — не забыть сохраниться!» В этой главе мы рассмот­рим, как организовать долговременное хранение данных в мобильном телефоне.

Хранение данных организовано в J2ME с помощью системы управления запися­ми (Record Management System), реализованной в пакете javax.microedition.rms. Данные представлены записями (массив байтов типа byte[]), которые помеща­ются в хранилище записей (Record Store) и не стираются при закрытии приложе­ния, перезагрузке телефона или замене батареи. Размер и физическое местополо­жение хранилища в памяти телефона зависят от конкретной модели аппарата. Хранилище жестко привязано к мидлету, и когда вы удаляете приложение из сво­его телефона, то все данные, относящиеся к нему, удаляются вместе с ним. По сути, хранилище записей — это база данных, которая может быть представлена следующей схемой (рис. 7.1).

Хранилище записей

"Test Storage"







"Test Storage"

"Blondes"

"SNAKE"

"SNAKE"



<record

<record

<record N>



1>


2>








N>

"Blondes"


1>


2>








N>

Рис. 7.1. Схема хранилища записей

RecordStore — хранилище записей 75

Применения для RMS могут быть разные, от сохранения параметров и пользова­тельских настроек приложения до поддержки полноценной адресной книги с ис­черпывающей личной информацией и гибким поиском. В игре, которую мы пи­шем, мы будем использовать систему управления записями для хранения уровня сложности игры, а также таблицы рекордов.



RecordStore — хранилище записей

Итак, хранилище записей представлено объектом класса RecordStore, работа с ко­торым начинается с метода открытия хранилища для дальнейшей работы static RecordStore openRecordStore(StringrecordStoreName. booleancreatelfNecessary). Первый аргумент метода — уникальное имя хранилища. Второй аргумент указывает, дол­жно ли быть создано новое хранилище с таким именем, если оно еще не существу­ет. Имя хранилища чувствительно к регистру, длина имени не должна превышать 32 символа.

Добавление записи вхранилище осуществляется с помощью метода intaddRecord(byte[] data, int offset, int numBytes). Сохраняемые данные должны быть представлены последовательностью байтов, содержащихся в массиве data. Переменная offset указывает, с какой позиции в массиве начинаются данные, предназначенные для записи, a numBytes определяет размер сохраняемых данных. Метод возвращает номер новой записи, присвоенный ей при сохранении (recordID).

Рассмотрим вкратце еще некоторые методы хранилища записей RecordStore:



  • void deleteRecorddnt recordld) — удалить из хранилища запись с номером
    record ld;

  • static voiddeleteRecordStore(StringrecordStoreName) - удалить хранилище за­
    писей с именем recordStoreName;

  • 1onggetLastModified() —возвращает время последнего изменения данных в хра­
    нилище записей в виде количества миллисекунд, прошедших с 1 января 1970 года
    до момента последнего изменения (Именно так!.. А кому сейчас легко?);

  • String getName() — получить имя хранилища данных;

  • 1nt getNextRecordID() — возвращает номер, который будет присвоен следующей
    добавленной записи;

  • int getNumRecordsO — возвращает количество записей, содержащихся на дан­
    ный момент в хранилище;

  • int getRecordSize( int recordld) — возвращает размер (в байтах) конкретной за­
    писи хранилища с номером recordld;

  • int getSizeO — возвращает объем памяти (в байтах), который занимают все
    записи хранилища;

  • int getSizeAvailableO — возвращает объем памяти (в байтах), доступный для
    расширения данного хранилища;

  • intgetVersion() — получить номер версии хранилища. При каждой операции
    с хранилищем, добавлении, удалении или изменении любой его записи, под­
    нимается номер версии хранилища;

76
Каталог: Техника -> Информационные%20технологии
Информационные%20технологии -> Методические рекомендации по построению систем защиты узлов интернет 1 требования к системе защиты узла интернет 2
Техника -> Учебная программа для специальности: 1-23 01 73 Средства массовой информации
Техника -> Ремонт китайских телефонов
Техника -> Для профилактики и лечения насморка Зачем промывать нос во время насморка?
Техника -> Учебная программа для специальности: 1-23 01 73 Средства массовой информации


Поделитесь с Вашими друзьями:
1   2   3   4   5   6   7   8   9   ...   15


База данных защищена авторским правом ©vossta.ru 2019
обратиться к администрации

    Главная страница