Лабораторная работа на тему Разработка Многопоточного приложения
« НазадЛабораторная работа на тему Разработка Многопоточного приложенияЦель работы: Научиться разрабатывать Android-приложения, использующие параллельную обработку данных на основе потоков. Теоретические сведенияМногопоточность
Фундамент, на котором построена ОС Android, - это аккуратно закодированная и кропотливо протестированная многопользовательская ОС Linux. Каждое приложение работает как отдельный пользователь в своем собственном процессе Linux. Linux и ее основные сервисы управляют техническими средствами смартфонов, планшетов, читалок электронных книг, умных часов, интерактивного TV (iTV), а также предоставляют Android-приложениям полный доступ к функциям каждого устройства, в том числе к процессорам, памяти, сенсорному экрану, хранилищам данных и многому другому. Одной из причин быстрого завоевания рынка системой Android также является тот факт, что Android использует язык Java в качестве основного языка программирования. Платформа разработки приложений Android является многослойным набором программных компонентов, включающих в себя тысячи библиотек функций на Java, которые функционируют сверху ядра Linux. Java-библиотеки упрощают задачу общения приложений с ядром Linux, которое является весьма сложным по своей сути. В частности, одним из таких сложных вопросов программирования является параллельная обработка информации с помощью нескольких потоков. Потоки являются краеугольным камнем любой многозадачной операционной системы и могут рассматриваться как мини-процессы, запущенные в рамках основного процесса с целью параллельного выполнения нескольких программных ветвей в пределах приложения. Большинство нетривиальных приложений Android используют несколько потоков, поэтому многопоточное программирования имеет важное значение для разработчиков для платформы Android. Многопоточность в ОС LinuxМногопоточность – это создание и управление несколькими единицами выполнения внутри одного процесса. Процесс – это набор компьютерных ресурсов, выделяемых операционной системой для выполнения откомпилированных программных файлов приложения, таких как код исполнительных файлов, оперативная память, объекты ядра ОС Linux и др. Потоки представляют собой последовательности выполнения кода в рамках процесса, каждой из которых назначаются виртуальный процессор, стековая память и текущее программное состояние. Другими словами, процессы выполняют двоичные файлы, а потоки - это наименьшая единица для планирования выполнения кода программ процессорами вычислительного устройства. Каждый процесс содержит один или несколько потоков. Если процесс содержит один поток, то есть только одну последовательность выполнения программного кода, то в процессе кроме этой последовательности выполнения ничего больше не может выполняться до самого завершения всего процесса. Такие процессы называются однопоточными и представляют собой классические процессы ОС Unix. Если процесс содержит более чем один поток, то более чем одна последовательность кода может выполняться одновременно. Такие процессы называются многопоточными, и в ОС Android, базирующейся на ОС Linux, подавляющее большинство приложений запрограммированы для выполнения с использованием многопоточных процессов. Практически все современные операционные системы предусматривают на уровне пользователей две основные виртуализированные абстракции: виртуальная память и виртуализированный процессор. Вместе они дают иллюзию для каждого запущенного процесса, что только он потребляет ресурсы компьютерного устройства. Виртуализированная память предоставляет каждому процессу линейное адресное пространство памяти, которое плавно отображается в физические оперативную и внешнюю память (за счет подкачки). Хотя оперативная память в действительности может в одно и то же время содержать данные до 100 различных выполняющихся процессов, но для каждого процесса создается иллюзия, что вся виртуальная память находится в его распоряжении. Виртуализированный процессор дает возможность процессу действовать так, как будто он один выполняется операционной системой, скрывающей от него тот факт, что она одновременно выполняет несколько процессов в режиме многозадачности на (возможно) нескольких процессорах ядрах. Виртуальная память ассоциируется с процессом, а не потоком, т. е., каждый процесс имеет свое пространство виртуальной памяти, а все потоки данного процесса пользуются этой памятью сообща. Наоборот, виртуализированный процессор ассоциируется с потоками, а не с процессами. Каждый поток представляет собой независимую единицу для выполнения операционной системой, что дает возможность одному процессу выполнять более чем одну программную ветвь приложения в одно и то же время. Основное преимущество многопоточных процессов таково: когда процесс должен выполнить много работы, потоки могут быть запущены одновременно на нескольких процессорах, что потенциально ускоряет вычисления. Хотя одновременные вычисления можно организовать и с помощью нескольких процессов, потоки запускаются быстрее процессов и потокам часто бывает проще и/или эффективнее взаимодействовать между собой при совместном использовании памяти по сравнению с процессами, которые взаимодействуют через сетевое соединение или канал. Основным недостатком является то, что многопоточность является существенным источником ошибок программирования из-за возникающих при этом гонок и взаимоблокировок параллельно выполняющихся потоков. Программирование потоков в JavaJava обеспечивает надежную встроенную поддержку для разработки многопоточных приложений, а создание новых потоков является относительно простым.Каждый поток представляется объектом класса, производным от базового класса java.lang.Thread, и, чтобы создать новый поток, надо просто определить класс, расширяющий класс Thread, или же реализующий интерфейс java.lang.Runnable. При разработке многопоточных приложений часто хочется создать класс с некоторой наследуемой функциональностью, код которого должен работать в отдельном потоке. Но этого нельзя сделать, так как этот новый класс должен наследовать и базовый класс Thread, а язык Java не допускает множественного наследования. Расширение класса Thread не дает никаких функциональных либо программных преимуществ по сравнению с реализацией новым классом интерфейса Runnable, так что второй вариант (реализация интерфейса Runnable) обычно оказывается предпочтительней. Единственным методом, определяемым в интерфейсе Runnable, является метод run(), который вызывается для выполнения потока. Как только поток выходит из метода run() (по завершению работы или вследствие необработанного исключения), он считается недействительным и не может быть перезапущен или повторно использован. Фактически метод run() выполняет ту же роль для потока, что и метод main() для Java-приложения: он является начальной точкой входа в код, выполняемый потоком. Как и в случае с методом main(), как правило, программа не должна вызывать метод run() напрямую. Вместо этого реализация интерфейса Runnable передается конструктору класса Thread, а поток вызовет метод run() автоматически в момент своего запуска. Например, чтобы выполнить какую-то подзадачу приложения в отдельном потоке, другом, чем основной поток приложения, нужно создать отдельный класс (скажем, Worker), реализующий интерфейс Runnable: class Worker implements Runnable { public void run() { // код, выполняющий подзадачу; } } Для того, чтобы использовать этот класс, достаточно создать новый объект класса Thread, передав его конструктору в качестве аргумента объект класса Worker, и, чтобы начать выполнение, вызвать метод start() класса Thread. Вызов start() задает, что вновь созданный поток должен начать выполнять код, вызвав метод run(), как было отмечено ранее: Thread t = new Thread(new Worker()); t.start(); Программирование многопоточных приложений в ОС Android. Когда один из компонентов Android-приложения, такой как активность, запускается на выполнение и приложение не имеет каких-либо других работающих в данное время компонентов, операционная система Android создает новый Linux-процесс для этого приложения, используя в нем только один поток выполнения, называемый основным потоком приложения или UI-потоком. Главное назначение основного потока – это управление пользовательским интерфейсом (UI), заключающееся в обработке системных событий и событий, вызванных взаимодействиями пользователя с элементами UI, а также в динамическом изменении представления элементов UI в ответ на эти события. Любые дополнительные компоненты, которые запускаются в рамках приложения, также, по умолчанию, будет работать в рамках главного потока выполнения. Любой компонент внутри Android-приложения, выполняющий трудоемкую по времени задачу в основном потоке, вызывает блокировку всего приложения до завершения этой задачи. Как правило, это приводит к отображению операционной системой предупреждения для пользователя «Приложение не отвечает». Очевидно, это далеко не желаемое поведение для любого приложения. Этого можно избежать, запуская трудоемкие задачи в отдельных рабочих потоках выполнения, давая возможность основному потоку беспрепятственно выполнять все остальные задачи приложения. Из вышесказанного вытекает одно из главных правил разработки для Android - никогда не выполнять трудоемкие операции в основном потоке приложения. Второе, не менее важное, правило заключается в том, что код в отдельном рабочем потоке никогда, ни при каких обстоятельствах не должен непосредственно обновлять любой элемент пользовательского интерфейса. Любые изменения в пользовательском интерфейсе всегда должны выполнятся из основного потока. Причиной последнего является то, что набор программных средств для построения пользовательского интерфейса Android-приложений не является потокобезопасным. Попытки выполнять потоконебезопасный код из нескольких потоков приводит, как правило, к непонятным ошибкам и непредсказуемому поведению приложения. В том случае, если коду, выполняющемуся в рабочем потоке необходимо взаимодействовать с пользовательским интерфейсом, он должен делать это, синхронизуя свою работу с основным потоком. Это достигается путем создания обработчиков в основной потоке, которые, в свою очередь, получают сообщения из других потоков и соответствующим образом обновляют пользовательский интерфейс. Для создания новых потоков можно использовать упомянутый выше класс Thread из базовой библиотеки Java. Но использование этого класса в Android-приложениях имеет ограничения – нельзя из вторичного потока изменять пользовательский интерфейс. Для решения этой проблемы в Android SDK имеется абстрактный параметризуемый класс AsyncTask: abstract class AsyncTask<Params, Progress, Result> { } Чтобы использовать этот абстрактный класс, необходимо сделать следующее: Создать класс, производный от AsyncTask, и реализовать его метод doInBackground(), который выполняется в пуле рабочих потоков. Для этого, как правило, создается внутренний класс в активности или фрагменте приложения: public class MyTask extends AsyncTask<String, Float, Boolean> { } Для изменения пользовательского интерфейса необходимо реализовать метод onPostExecute() класса AsyncTask, который получает результаты, возвращенные методом doInBackground(), и выполняется в основном потоке, так что в нем можно надежно обновлять пользовательский интерфейс. После этого, чтобы выполнить в рабочем потоке задачу, реализованную в методе doInBackground(), нужно из основного потока вызвать метод execute() объекта класса, производного от AsyncTask. Кроме перечисленных выше, класс AsyncTask содержит еще несколько методов для взаимодействия основного и рабочего потоков, которые можно переопределять по мере необходимости: Метод onPreExecute(), который вызывается из основного потока перед запуском метода doInBackground(). Метод onProgressUpdate(), выполняющийся в основном потоке и обрабатывающий данные о выполнении рабочего потока, передаваемые ему методом publishProgress(). Метод publishProgress(), вызываемый из выполняющегося в рабочем потоке метода doInBackground() для передачи данных, вычисленных или полученных в рабочем потоке, методу основного потока onProgressUpdate(). Используя класс AsyncTask, основной поток может запустить несколько одновременно выполняющихся рабочих потоков, переопределяя описанные выше методы в производных от AsyncTask классах. Но такое выполнение порождает ряд проблем, связанных с конкурентным использованием потоками общих ресурсов, а также с отсутствием гарантий, что из-за конкуренции потоки будут завершаться в том же порядке, в котором они были запущены. Как следствие, начиная API level 11 в реализацию класса AsyncTask были внесены изменения, в результате которых все задачи, запускаемые методом execute(), по умолчанию выполняются последовательно одним рабочим потоком, а для выполнения задач параллельными потоками был добавлен новый метод: public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec, Params… params) Реализации используемого в этом методе интерфейса Executor из пакета java.util.concurrent могут выполнять задачи последовательно, используя единственный рабочий поток, параллельно с использованием ограниченного по числу пулом потоков или же непосредственно создавая новый поток для каждой задачи. Таким образом, оба представленные ниже вызовы методов: task.execute(params); task.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, params); делают одно и то же, последовательно выполняя все запускаемые задачи на одном рабочем потоке. Параллельное же выполнение задач пулом потоков обеспечивает вызов метода: task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params); Пул потоков в классе AsyncTask конфигурируется таким образом, чтобы в нем было по крайней мере 5 параллельно работающих потоков с дальнейшим расширением по мере необходимости до 128, что вполне достаточно для подавляющего большинства приложений, реализуемых на платформе Android. Для сокращения записи вызовов методов класса AsyncTask, а также обеспечения обратной совместимости в API level 4 был введен оберточный класс AsyncTaskCompat. В частности, добавление нового потока в пул параллельно выполняющихся потоков обеспечивает статический метод: AsyncTaskCompat.executeParallel(task, params); который может использоваться вместо метода executeOnExecutor() класса AsyncTask. ЗаданиеНа рис. 2 представлено изображение множества Мандельброта - фрактала, определённого как множество точек на комплексной плоскости, для которых не уходит в бесконечность итеративная последовательность: Z0 = 0 Zn+1 = Zn2 + C Рисунок 1. Представление на экране Android-устройства изображения множества Мандельброта Если получать приведенное на рисунке изображение последовательно с помощью одного потока, то на его получение и отображение на экране устройства понадобиться порядка 10 секунд. Так как итерации для получения изображения ведутся построчно, вычисления можно распределить между группой параллельно работающих потоков, итерирующих соседние строки изображения. В результате время получения множества в заданных границах и его отображения на экране может уменьшится в несколько раз, если Android-устройство построено на базе кристалла с несколькими процессорными ядрами. В программе должно быть предусмотрено автоматическое масштабирование изображения в зависимости от размеров окна браузера. Кроме того, после касания какой-то точки изображения должны устанавливаться новые границы множества Мандельброта с коэффициентом масштабирования, равным 8, и с помощью той же группы параллельно работающих потоков должен вычисляться новый фрактал, поддерживающий соотношение сторон текущего размера окна. На рис.2 представлен один из таких фракталов. ВыводыМодель параллельных вычислений на основе потоков, используемая в приложениях для платформы Android, базируется на свойствах и методах параметризуемого класса AsyncTask. В этой модели все длительные по времени вычисления выполняются одним либо несколькими рабочими потоками, что дает возможность выполнять обработку параллельно несколькими процессорными ядрами. Вся работа по синхронизации рабочих потоков, исполнению потоконебезопасных участков приложения, а также визуализация результатов обработки на экране устройства выполняются основным потоком, с которого начинается работа при запуске приложения. Список литературы
Приложение Множество МандельбротаМножество Мандельброта (один из самых известных фрактальных объектов) впервые было построено (визуально с применением ЭВМ) Бенуа Мандельбротом весной 1980 г. в исследовательском центре фирмы IBM им. Томаса Дж. Уотсона (см. [1]). И хотя исследования подобных объектов начались ещё в прошлом веке, именно открытие этого множества и совершенствование аппаратных средств машинной графики в решающей степени повлияли на развитие фрактальной геометрии и теории хаоса. Итак, что же такое множество Мандельброта. Рассмотрим функцию комплексного переменного . Положим и рассмотрим последовательность , где для любого . Такая последовательность может быть ограниченной (т.е. может существовать такое r, что для любого ) либо "убегать в бесконечность" (т.е. для любого r>0 существует ). Множество Мандельброта можно определить как множество комплексных чисел c, для которых указанная последовательность является ограниченной. К сожалению, не известно аналитического выражения, которое позволяло бы по данному c определить, принадлежит ли оно множеству Мандельброта или нет. Поэтому для построения множества используют компьютерный эксперимент: просматривают с некоторым шагом множество точек на комплексной плоскости, для каждой точки проводят определённое число итераций (находят определённое число членов последовательности) и смотрят за её "поведением". Доказано, что множество Мандельброта размещается в круге радиуса r=2 с центром в начале координат. Таким образом, если на некотором шаге модуль очередного члена последовательности превышает 2, можно сразу сделать вывод, что точка, соответствующая c, определяющему данную последовательность, не принадлежит множеству Мандельброта. Уменьшая шаг, с которым просматриваются комплексные числа, и увеличивая количество итераций, мы можем получать сколь угодно подробные, но всегда лишь приближённые изображения множества. Пусть в нашем распоряжении имеется N цветов, занумерованных для определённости от 0 до N-1. Будем считать, опять же для определённости, что черный цвет имеет номер 0. Если для данного c после N-1 итераций точка не вышла за круг радиуса 2, будем считать, что c принадлежит множеству Мандельброта, и покрасим эту точку c в чёрный цвет. Иначе, если на некотором шаге k (k Є [1; N-1]) очередная точка вышла за круг радиуса 2 (т.е. на k-ом шаге мы поняли, что она "убегает"), покрасим её в цвет k. Красивые изображения получаются при удачном выборе палитры и окрестности множества (а именно вне множества мы и получим "цветные точки"). Примеры: z=z*z+c, Окно: (-2; -2) - (2; 2) z=z*z+c, Окно: (-1.80991280915827; -4.76670952784024E-5) - (-1.80991280652736; -4.76644643672749E-5) z=z*z+c, Окно: (0.05485; 0.65365) - (0.0607; 0.6595) Множество ЖюлиаМножества Жюлиа, тесно связанные с множеством Мандельброта, были исследованы ещё в начале XX века математиками Гастоном Жюлиа и Пьером Фату (см. [1]). В 1917-1919 гг. ими были получены основополагающие результаты, связанные с итерированием функций комплексного переменного. Вообще говоря, этот факт заслуживает отдельного обсуждения и является впечатляющим примером математического исследования, на многие десятилетия опередившего время (учёные могли лишь приблизительно представлять, как выглядят исследуемые ими объекты!), но мы опишем лишь способ построения множеств Жюлиа для функции комплексного переменного . Говоря более точно, мы будем строить т.н. "заполняющие множества Жюлиа". Рассмотрим прямоугольник (x1;y1)-(x2;y2). Зафиксируем константу c и станем просматривать точки выбранного прямоугольника с некоторым шагом. Для каждой точки, как и при построении множества Мандельброта, проведём серию итераций (чем больше число итераций, тем точнее будет получено множество). Если после серии итераций точка не "убежала" за границу круга радиуса 2, поставим её чёрным цветом, иначе цветом из палитры. Примеры: z=z*z+c, Окно: (-2; -2) - (2; 2), c= (-0.74543; 0.11301) z=z*z+c, Окно: (-0.9; 0.12) - (-0.64; 0.38), c= (-0.74543; 0.11301) z=z*z+c, Окно: (-0.0858; -0.0898) - (0.0946; 0.0906), c= (-0.74543; 0.11301) Литература к приложению1. Беленькая Н.Л., Сергеев Л.О. Фракталы. // Информатика (прилож. к "Первое сентября"). - 2000. - N 30. 2. Bведение во фракталы, http://fractals.narod.ru/intro.htm 3. Жиков В. В. О множествах Жюлиа. // Современное естествознание: Энциклопедия: В 10 т. Т.1: Математика. Механика. М., 2000. 4. Жиков В. В. Фракталы. // Современное естествознание: Энциклопедия: В 10 т. Т.1: Математика. Механика. М., 2000. 5. Кроновер Р. М. Фракталы и хаос в динамических системах. Основы теории. - М: Постмаркет, 2000. 6. Мандельброт Б. Фрактальная геометрия природы. – М: Институт компьютерных исследований, 2002. З повагою ІЦ "KURSOVIKS"!
|