21-10-2023
Grand Central Dispatch (GCD), намек на название центрального вокзала в Нью-Йорке [1]. Впоследствии библиотека была портирована[2] на другую операционную систему FreeBSD [3].
GCD позволяет определять задачи в приложении, которые могут параллельно выполняться, и запускает их при наличии свободных вычислительных ресурсов (процессорных ядер)[4].
Задача может быть определена как функция, либо как «блок».[5] Блок — это нестандартное расширение синтаксиса языков программирования C/C++/Objective-C, позволяющее инкапсулировать код и данные в один объект, аналог замыкания.[4]
Grand Central Dispatch использует потоки на низком уровне, но скрывает детали реализации от программиста. Задачи GCD легковесны, недороги в создании и переключении; Apple утверждает, что добавление задачи в очередь требует лишь 15 процессорных инструкций, в то время как создание традиционного потока обходится в несколько сотен инструкций.[4]
Задача GCD может быть использована для создания рабочего элемента, который помещается в очередь задач, либо может быть привязана к источнику события. Во втором случае при срабатывании события задача добавляется в соответствующую очередь. Apple утверждает, что этот вариант более эффективен, нежели создавать отдельный поток, ожидающий срабатывание события.
Содержание |
Платформа GCD объявляет несколько типов данных и функций для создания и манипулирования ими.
Два примера демонстрирующие простоту использования Grand Central Dispatch могут быть найдены в обзоре Snow Leopard Джона Сиракуза на Ars Technica.[6].
Изначально, у нас имеется приложение с методом analyzeDocument, осуществляющим подсчет слов и параграфов в документе. Обычно, процесс подсчета слов и параграфов достаточно быстр и может быть выполнен в главном потоке, без опасений, что пользователь заметит задержку между нажатием кнопки и получением результата:
- (IBAction)analyseDocument:(NSButton *)sender { NSDictionary *stats = [myDoc analyse]; [myModel setDict:stats]; [myStatsView setNeedsDisplay:YES]; [stats release]; }
Если документ очень большой, то анализ может занять достаточно много времени, чтобы пользователь заметил «подвисание» приложения. Следующий пример позволяет легко решить эту проблему:
- (IBAction)analyzeDocument:(NSButton *)sender { dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSDictionary *stats = [myDoc analyze]; dispatch_async(dispatch_get_main_queue(), ^{ [myModel setDict:stats]; [myStatsView setNeedsDisplay:YES]; [stats release]; }); }); }
Здесь вызов [myDoc analyze] помещен в блок, который затем помещается в одну из глобальных очередей. После того, как [myDoc analyze] завершает работу, новый блок помещается в главную очередь, который обновляет интерфейс пользователя. Проведя эти несложные изменения, программист избежал потенциального «подвисания» приложения при анализе больших документов.
Второй пример демонстрирует распараллеливание цикла:
for (i = 0; i < count; i++) { results[i] = do_work(data, i); } total = summarize(results, count);
Здесь вызывается функция do_work count раз, результат ее i-го выполнения присваивается i-му элементу массива results, затем результаты суммируются. Нет оснований полагать, что do_works полагается на результаты ее предыдущих вызовов, поэтому ничто не мешает делать несколько вызовов do_works параллельно. Следующий листинг демонстрирует реализацию этой идеи с помощью GCD:
dispatch_apply(count, dispatch_get_global_queue(0, 0), ^(size_t i){ results[i] = do_work(data, i); }); total = summarize(results, count);
В этом примере dispatch_apply запускает count раз блок, переданный ему, помещая каждый вызов в глобальную очередь и передавая блокам числа от 0 до count-1. Это позволяет ОС выбрать оптимальное число потоков для наиболее полного использования доступных аппаратных ресурсов. dispatch_apply не возвращает управление, пока все его блоки не завершили работу, это позволяет гарантировать, что перед вызовом summarize вся работа изначального цикла выполнена.
Разработчик может создать отдельную последовательную очередь для задач, которые должны выполняться последовательно, но могут работать в отдельном потоке. Новая очередь может быть создана таким образом:
dispatch_queue_t exampleQueue; exampleQueue = dispatch_queue_create( "com.example.unique.identifier", NULL ); // exampleQueue может быть использована здесь. dispatch_release( exampleQueue );
Избегайте помещение такой задачи в последовательную очередь, которая помещает другую задачу в ту же самую очередь. Это гарантированно приведет к взаимоблокировке (deadlock). В следующем листинге продемонстрирован случай такой взаимоблокировки:
dispatch_queue_t exampleQueue = dispatch_queue_create( "com.example.unique.identifier", NULL ); dispatch_sync( exampleQueue, ^{ dispatch_sync( exampleQueue, ^{ printf( "I am now deadlocked...\n" ); }); }); dispatch_release( exampleQueue );
Диспетчер Grand Central.