Многопоточность в iOS

Рассмотрим немного теории о многопоточности.

Терминология:

Поток – отдельная нить исполняемого кода в приложении. Каждый поток содержит собственный стек. Но всю остальную часть памяти поток делит с другими потоками того же процесса.

Процесс – запущенное приложение, которое может содержать много потоков.

Задача – абстрактная концепция некоторого количества выполняемой работы.

Вообще работа непосредственно с потоками не является лучшим выбором для приложений с распределенной архитектурой. Технологии идут вперед, и сейчас перед нами появляется выбор более высокоуровневых концепций. В OS X и iSO эти концепции представлены асинхронным дизайном (asynchronous design approach).

Одна из технологий для запуска асинхронных задач – Grand Central Dispatch (GCD). GCD предоставляет более высокоуровневый доступ к потокам. Все, что нам остается сделать – определить задачу, которую мы хотим асинхронно выполнить, и добавить ее в очередь посылок (dispatch queue).

Еще одна технология, представленная в виде объектов Objective-C – операционные очереди (operation queues). Они очень похожи по своей сути на GCD.

Каждый процесс (приложение) в Mac OS X или iOS состоит из одного или нескольких потоков, каждый из которых представляет собой отдельный путь выполнения с помощью кода приложения. Каждое приложение начинается с одного потока, который порождает main функция приложения. Приложения могут порождать дополнительные потоки, каждый из которых выполняет код конкретной функции.

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

В замен прямого создания и использования потоков Mac OS или iOS примитяет на вооружение асинхронный подход к программированию. Асинхронные функции присутствуют в операционных системах в течение многих лет и часто используются для реализации задач, которые могут занять много времени, такие как чтение данных с диска. При вызове асинхронная функция делает какую-то работу за кулисами, чтобы запустить задачу, но возвращается раньше, чем эта задача может быть фактически завершена. Как правило, эта работа включает в себя создание фонового потока, начиная с выполнения задачи в этом потоке, а затем отправляет уведомление абоненту (обычно с помощью функции обратного вызова), когда задача выполнена.

Одной из технологий для запуска задач асинхронно это 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. Первый аргумент которой — приоритет очереди, а второй зарезервирован на будущее.

У нас появилось новое понятие — очередь, мы его рассмотрим позже. Пока лишь определимся, что у нас есть фоновая очередь (их всего 4) и главная (она выполняется в главном потоке).

Выполняем код в фоне и вызываем главный поток

Еще одна очень популярная задача — выполнить некоторый код в фоне, а затем выполнить другой код в главном потоке. Смотрим, как это сделать с помощью GCD:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    /* Код, который должен выполниться в фоне */

 

dispatch_async(dispatch_get_main_queue(), ^{ /* Код, который выполниться в главном потоке */ });

});

Ждем выполнения задачи

Следующий пример демонстрирует ситуацию, когда нужно подождать выполнения другой задачи. Для этого существует функция dispatch_sync.

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    /* Код, который должен выполниться в фоне */

 

dispatch_sync(dispatch_get_main_queue(), ^{ /* Код, который нужно выполнить в главном потоке */ /* Мы ждем, пока он выполниться */ });

/* Продолжаем работать в фоновом потоке */ });

Неплохо, правда? Вспомните performSelectorInBackground:withObject: и NSOperationQueue. С GCD вам не нужно заботиться о создании потоков!

Очереди

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);

Обратите внимание на имя, задаваемое очереди — «com.myapp.myqueue». При создании его можно и не указывать, но имя облегчает отладку приложения. В дебагере будет показано это имя при отладке. Так же имя будет показано в crashlog-ах.

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-очередей, созданных вами.

Мы увидели GCD в действии и познакомились с двумя функциями:

  • dispatch_async — ставит задачу на выполнение и возвращает управление немедленно
  • dispatch_sync — ставит задачу на выполнение и ожидает выполнения, после чего, возвращает выполнение

dispatch_once

Функция dispatch_once используется, когда конкретную задачу нужно выполнить только один раз за все время жизни приложения. Это может быть полезным, например, при инициализации синглтона.

С помощью GCD и функции dispatch_once этот код можно заменить вот таким:

static dispatch_once_t task;
dispatch_once(&task, ^{
    // код
});

dispatch_after

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

double delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
    // код
});

Обратите внимание на то, что нет гарантий, что код запуститься сразу по истечении времени. Когда пройдет время, задача станет в очередь на выполнение, а начнет она выполняться или нет — зависит уже от нагруженности самой очереди. Но, обычно, задачи стартуют во время или с небольшой погрешностью.

Время, когда запускать задачу на выполнение представлено структурой dispatch_time_t, которая создается функцией dispatch_time. Она определяет сколько времени (второй аргумент), прошло от конкретного времени (первый аргумент). В данном случае мы используем текуще время DISPATCH_TIME_NOW и отсчитываем от него две секунды. Время отсчета задается в нано секундах. Если мы хотим использовать время в секундах, то нам нужно умножать количество секунд на NSEC_PER_SEC (наносекунд в секунде). Если мы хотим отсчитать милисикунды, можно использовать NSEC_PER_MSEC).

dispatch_apply

Эта функция используется для запуска задачи на выполнение несколько раз. Функция возвращает управление когда все запущенные задачы будут закончены.

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, queue, ^(size_t index) {
   // блок выполнится 10 раз
   // в блок передается номер запуска
});

dispatch_apply распаралеливает выполнение задач. Например, если запустить вот такой код:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, queue, ^(size_t index) {
    NSLog(@"%@ , %zu",[NSDate date], index);
});

В консоли мы можем увидеть вот такое:

2012-06-13 19:08:31 +0000 , 0
2012-06-13 19:08:31 +0000 , 2
2012-06-13 19:08:31 +0000 , 3
2012-06-13 19:08:31 +0000 , 4
2012-06-13 19:08:31 +0000 , 5
2012-06-13 19:08:31 +0000 , 6
2012-06-13 19:08:31 +0000 , 7
2012-06-13 19:08:31 +0000 , 8
2012-06-13 19:08:31 +0000 , 9
2012-06-13 19:08:31 +0000 , 1

Это говорит о том, что задачи действительно распарателиваются.

На этом все. В следующих статьях мы рассмотрим методы управления очередями и механизм семафоров в GCD.

 

Остановка и запуск очереди

Чтобы очередь не начинала выполнять новые задачи мы можем использовать функцию dispatch_suspend:

dispatch_suspend(queue);

После вызова этой функции очередь перестанет запускать новые задачи на выполнение. Задачи, которые уже выполняются продолжат выполняться.

Чтобы вернуть очередь к жизни нужно использовать функцию dispatch_resume:

dispatch_resume(queue);

Источники: Многопоточность в iOS. Введение в GCD, Распределенное программирование и многопоточность в iOS. Обзор доступных инструментов,
Многопоточное программирование в среде Cocoa, Mac OS, iOS Управление потоками.
Продолжение следует...
Вы можете оставить комментарий, или ссылку на Ваш сайт.

Оставить комментарий