Поток как системный объект Характеристика, атрибуты, использование
Каждое iOS приложение состоит из одного или более потока. Каждое приложение начинается с одного потока и, затем может создавать еще дополнительные потоки. Когда приложение создает дополнительный поток, он становится отдельной сущностью в пространстве процесса приложения. Каждый поток имеет свой стек и планируется на исполнение отдельно kernel’ом. Поток может общаться с другими потоками. Все потоки находятся в общем адресном пространстве приложения и делят одну и ту же виртуальную память и имеют те же права доступа что и процесс приложения. Главный поток приложения имеет существенные отличия от остальных по своему фнукционалу. Он выполняет функцию main приложения и отвечает за обработку событий от пользователя и обновление UI. Поэтому, если, скажем нам нужно сделать чтото асинхронное или объемное, то желательно это сделать не в основном потоке. Если это делать на главном потоке – приложение перестанет реагировать на пользователя и с большой долей вероятности будет закрыто. Пото́к выполне́ния (тред; от англ. thread — нить) — наименьшая единица обработки, исполнение которой может быть назначеноядром операционной системы. Многопото́чность — свойство платформы (например, операционной системы, виртуальной машины и т. д.) или приложения, состоящее в том, что процесс, порождённый в операционной системе, может состоять из нескольких потоков, выполняющихся «параллельно». Процесс (process) — не что более иное, как некая абстракция, которая инкапсулирует в себе все ресурсы процесса (открытые файлы, файлы отображенные в память...) и их дескрипторы, потоки и т.д. Каждый процесс имеет как минимум один поток. Каждое приложение начинается с одного потока, который порождает main функция приложения. Приложения могут порождать дополнительные потоки, каждый из которых выполняет код конкретной функции. Когда приложение порождает новый поток, этот поток становится независимым органом внутри пространства процесса приложения. Каждый поток имеет собственный стек исполнения и запланирован на выполнение отдельно от ядра. Поток может общаться с другими потоками и другими процессами, выполнять операции ввода/вывода, и делать все, что, возможно, придется делать. Поскольку они в одном пространстве с процессом, все потоки в одном приложении используют одно и то же виртуальное пространство памяти и имеют одни и те же права доступа, как и сам процесс.
Каждый поток, как и каждый процесс, имеет свой контекст. Контекст — это структура, в которой сохраняются следующие элементы: ● Регистры процессора. ● Указатель на стек потока/процесса. Регистр процессора — блок ячеек памяти, образующий сверхбыструю оперативную память (СОЗУ) внутри процессора; используется самим процессором и большой частью недоступен программисту: например, при выборке из памяти очередной команды она помещается в регистр команд , к которому программист обратиться не может.
Использование POSIX потоков Эта технология может быть использована в любом типе приложения и это может быть более удобно, если вы пишете программы для различных платформ. Процедура POSIX, называется, pthread_create. POSIX (англ. portable operating system interface for Unix — переносимый интерфейс операционных систем Unix) — набор стандартов, описывающих интерфейсы между операционной системой и прикладной программой (системный API), библиотеку языка C и набор приложений и их интерфейсов. Стандарт создан для обеспечения совместимости различных UNIX-подобных операционных систем и переносимости прикладных программ на уровне исходного кода, но может быть использован и для не-Unix систем. Серия стандартов POSIX была разработана комитетом 1003 IEEE. Международная организация по стандартизации (ISO) совместно c Международной электротехнической комиссией (IEC) приняли данный стандарт (POSIX) под названием ISO/IEC 9945. Рассмотрим пример приведенного loading-screen'a максимально упрощенной реализации на С. Пикча:
Как видим, сначала создаем рабочие структуры хранения информации о потоках типа pthread_t – так выглядят потоки для пользователя API Posix. Далее иницилизируем их с помощью функции pthread_create, где первым аргументом идет указатель на дексриптор потока, вторым аргументом идет переменная параметром потока, которая выше nil, третьим аргуметом – функция, которую выполняет поток, и четвертым параметром – аргументы, которые передаются в это функцию, тоже nil (nil мне кажется более синтаксически приятным чем NULL, тем более привычка). Аргумент и возвращаемые параментры функции, которую выполняет поток, должны быть void* — т.е. сырой указатель на данные. Сразу после того, как был вызван pthread_create, поток отделяется и начинает выполнять свою функцию. Т.е. все, что после pthread_create происходит в функции – происходит параллельно main потока (в том котором выполняется main функция). pthread_join – функция, которая нужна для того, чтобы main поток не завершился сразу после создания тех двух, а ждал их завершения. В примере выше сначала ждем завершение потока loader, а потом drawer. Но обычно не важен порядок ожидания потоков, если ничего после этого не выполняем. Использование NSThread Есть два способа создания потока c использованием класса NSThread: ● Использование метода класса detachNewThreadSelector:toTarget:withObject:, чтобы породить новый поток. ● Создание нового объекта NSThread и вызов его метода start. Оба метода создают отдельные потоки в приложении. Отдельный поток означает, что ресурсы потока будут автоматически утилизированы системой при выходе из него. Это также означает, чтокод не должен присоединяться в явном виде к потоку позже. Поскольку detachNewThreadSelector:toTarget:withObject: метод поддерживается всеми версиями Mac OS X, он часто встречается в существующих Cocoa приложениях, использующих потоки. Чтобы отключить новый поток, просто укажите имя метода (указывается в качестве селектора), который хотите использовать в качестве исходной точки потока, объект, который определяет, этот метод, и любые данные, которые хотите передать в поток при запуске . В следующем примере показаны основы вызова этого метода, для порождения потока, используя специальный метод текущего объекта. [NSThread detachNewThreadSelector:@selector(myThreadMainMethod:) toTarget:self withObject:nil]; Простой способ инициализации NSThread объекта в Mac OS X 10.5 и более поздних является использование метода initWithTarget:selector:object:. Этот метод имеет точно такую же информацию, как метод detachNewThreadSelector:toTarget:withObject: и использует его для инициализации нового экземпляра NSThread. Однако он не запускает поток. Чтобы запустить поток, вызываете метод start объекта потока в явном виде, как показано в следующем примере: NSThread* myThread = [[NSThread alloc] initWithTarget:self selector:@selector(myThreadMainMethod:) object:nil];
[myThread start]; // На самом деле создает поток
NSOperation В Foundation есть абстрактный класс NSOperation, который позволяет «обернуть» задачу в класс и выполнить операцию (задачу). Кроме абстрактного NSOperation существуют еще NSBlockOperation и NSInvocationOperation. В чем разница? При использовании абстрактного класса NSOperation нам нужно создавать отдельный класс, унаследовать NSOperation и реализовать задачу. NSBlockOperation позволяет «обернуть» один или несколько блоков кода, т.е. нет необходимости наследования. NSInvocationOperation позволяет создать операцию из уже существующего метода. Операции могут быть асинхонными (non-concurrent) и синхронными (concurrent). У слова concurrent/non-concurrent есть другой перевод, но мне кажется синхронный/асинхронный понятнее. Существует два метода запуска операций на выполнение. 1) NSOperationQueue 2) Запуск вручную При использовании NSOperationQueue учитывается зависимость операций и приоритет, а так же обеспечивается асинхронность операции. Очереди можно приостанавливать и снова запускать. Если мы запускаем задачу «вручную» то она выполняется синхронно, блокируя поток вызова до тех пор, пока операция не закончится. Создадим операцию при помощи наследования от NSOperation. Для этого создадим класс MyOperation: MyOperation.h #import <foundation/Foundation.h>
@interface MyOperation : NSOperation {
}
@end MyOperation.m #import "MyOperation.h"
@implementation MyOperation
-(void) main { // задача которая требует некоторое кол-во времени // поток заснет на некоторое время sleep(3); }
@end Обязательным условием является определение метода main в классе. Именно он будет выполняться асинхронно. Так же можно определить свой конструктор, например, для передачи каких-либо параметров в операцию. Можно добавить свойства. Как я говорил выше, одним из способов запуска операций является NSOperationQueue. Делается это так: NSOperationQueue* queue = [NSOperationQueue new]; MyOperation* op = [MyOperation new];
[queue addOperation:op]; «Вручную» запустить операцию очень просто. Достаточно вызвать у нее метод start. Операция начнет немедленно выполняться и заблокирует поток вызова до момента своего завершения.
GCD или Grand Central Dispatch Механизм распаралеливания задач, представленный в iOS 4 и Mac OS X 10.6. Суть механизма в том, что реализация многопоточности скрывается от программиста. Всю «заботу» о создании потоков берет на себя GCD. Утверждается, что задачи GCD легковесны и требуют меньше процессорного времени, чем создание потоков. Получается, что все что требуется от программиста — определить какие задачи выполнять и поставить в нужную очередь, а GCD уже разберется со всем остальным. Пример использования кода в фоне, который чаще всего будет встречаться в приложениях, использующих GCD. Выполним код в фоновом потоке: dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ /* Код, который должен выполниться в фоне */ }); Функция dispatch_async требует указать очередь, в которой должен быть выполнен код, который в свою очередь представлен блоком. Блок так же идет на вход функции dispatch_async. Блок ничего не принимает на вход и ничего не возвращает. Функция dispatch_async возвращает управление немедленно и ставит блок на выполнение в очереди. Чтобы получить нужную очередь, обычно используют функцию dispatch_get_global_queue. Первый аргумент которой — приоритет очереди, а второй зарезервирован на будущее. Concurrent-очереди Как вы уже могли заметить, нужно ставим блоки с кодом на выполнение в конкретной очереди. Очередь представлена типом dispatch_queue_t. Очередь можно создать с помощью функции dispatch_queue_create, но, я думаю, что в большинстве случаев этого делать не нужно. GCD «из коробки» предоставляет нам пять очередей. Одна из них — главная очередь, которая исполняется в главном потоке. Получить главную очередь можно через функцию dispatch_get_main_queue. Остальные четыре разделены по приоритетам. Посмотрите на первый пример с кодом — в фунцкцию dispatch_get_global_queue передаем приоритет очереди, которую хотим получить. Вот они: DISPATCH_QUEUE_PRIORITY_HIGH DISPATCH_QUEUE_PRIORITY_DEFAULT DISPATCH_QUEUE_PRIORITY_LOW DISPATCH_QUEUE_PRIORITY_BACKGROUND Все описанные выше очереди это так называемые Concurrent-очереди. Задачи в них выполняются параллельно. В таких очередях GCD сам управляет потоками и создает нужно количество потоков для выполнения задач. Concurrent-очередь можно создать так: dispatch_queue_t queue = dispatch_queue_create("com.myapp.myqueue", DISPATCH_QUEUE_CONCURRENT);
Serial-очереди Кроме concurrent-очередей существуют еще и Serial-очереди, задачи в которых выполняются последовательно. Для каждой serial-очереди создается отдельный поток. Serial-очередь выполняет только одну задачу. После того, как очередная задача выполнена — берется следующая, поставленная в очередь на выполнение. Эта очередь работает по принципу FIFO (First in — First out, «Первый пришел — первый ушел»). Но не стоит увлекаться созданием большого количества Serial-очередей так как будет создано большое количество потоков и это может «тормозить» приложение. Serial-очереди могут пригодиться, когда вам нужно разграничить доступ к данным для потоков, когда нельзя чтобы потоки одновременно записывали одну и ту же область памяти. Serial-очередь создается вот так: dispatch_queue_t serialQueue = dispatch_queue_create("com.myapp.queue", NULL); Когда очередь вам больше не нужна, для нее следует вызвать функцию dispatch_release. dispatch_release(serialQueue); Это же касается и Concurrent-очередей.