Многопоточность в iOS. Введение в GCD

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 — ставит задачу на выполнение и ожидает выполнения, после чего, возвращает выполнение
Так же мы узнали о типах очередей и очередях, которые существуют в системе. Все выше прочитанное можно использовать для реализации многопоточности в ваших приложениях и для некоторых приложений этого будет достаточно.
Спасибо за внимание, в следующей части мы продолжим знакомиться с GCD.