Основы Grand Central Dispatch, часть первая: общая информация и очереди посылок
Автор: Mike Ash; оригинал статьи: Intro to Grand Central Dispatch, Part I: Basics and Dispatch Queues
Аннотация
На этой неделе (оригинальный текст был опубликован в конце сентября 2009 года, прим. пер.) Apple выпустила новый релиз ОС, Snow Leopard, поэтому я собираюсь возпользоваться возможностью и открыть обсуждение ранее закрытой (Apple Developer NDA обычно действует до момента выхода релиза продукта, прим. пер.) технологии и поговорить о клевых вещах, которые появились в новой версии операционной системы. С этой недели я планирую начать серию, посвященную Grand Central Dispatch, теме, которую предложил Chris Liscio.
Что это?
Grand Central Dispatch, или, коротко, GCD — это низкоуровневое API, которая открывает новый способ работать с параллельными (оригинально это concurrent, а не parallel, я не знаю нормального перевода, если кто скажет — напишите в комментариях, прим. пер.) программами. На самом простом уровне понимания, методология похожа на NSOperationQueue, которая позволяет разбивать программу на независимые задачи, которые запускать параллельно или последовательно. GCD работает на более низком уровне, предоставляет большую производительность и не является частью Cocoa.
В дополнение к средствам параллельного выполнения кода, GCD также предоставляет полностью интегрированную систему обработки событий. Обработчики могут быть сконфигурированы таким образом, чтобы реагировать на события от файловых дескрипторов, системных портов и процессов, таймеров и сигналов, и на пользовательские события. Эти обработчики исполняются параллельно при помощи инфраструктуры GCD.
API GCD полностью основан на так называемых блоках, о которых я говорил в предыдущих сериях ответов на вопросы («Позвольте представить»: блоки и «Обсуждение практических аспектов использования блоков в обычном коде»). GCD можно использовать и без блоков, применяя традиционные C-шные механизмы указателей на функции и контекста, но использовать блоки гораздо проще и невероятно удобнее с практической точки зрения.
Для получения системной документации по GCD, можно набрать man dispatch в командной строке, если у вас Snow Leopard.
Зачем это нужно использовать?
GCD дает множество преимуществ перед традиционным многопоточным программированием:
- Простота использования. GCD использовать гораздо проще, чем потоки. Так как основой являются элементы работы, а не потоки вычислений, можно просто запустить на выполнение работу, не заботясь о стандартных задачах, таких, как ожидание завершения работы, отслеживание дескрипторов файлов, исполнение кода с какой-то периодичностью, временная приостановка работы. API, основанные на блоках позволяют с необычайной легкостью передавать контекст между различными частями кода.
- Эффективность. GCD реализован легковесным способом, что делает гораздо более практичным и быстрым использование GCD в тех местах, где выделенные потоки слишком затратны. С этим тесно связана простота использования: отчасти, то, что позволяет так легко использовать GCD, позволяет и не сильно при его использовании заботиться об эффективности.
- Производительность. GCD автоматически масштабируется (создает и удаляет потоки) в соответствии с загрузкой системы, что ведет за собой меньшее количество переключений контекста и большую вычислительную эффективность.
Посылки (Dispatch Objects)
Не смотря на то, что GCD — это чистый C, он спроектирован в объектном стиле. Объекты GCD называются посылками (Dispatch Objects). Посылки используют подсчет ссылок, как и объекты Cocoa. Для подсчета ссылок (контроля за памятью) можно использовать функции dispatch_retain и dispatch_release. Стоит помнить, что, в отличие от объектов Cocoa, посылки не участвуют в сборке мусора, то есть ими нужно управлять вручную, даже если включен сборщик мусора (к iOS это по-прежнему пока не относится, прим. пер).
Очереди посылок и источники посылок (что это — чуть позднее) могут быть приостановлены и запущены снова, могут содержать произвольный указатель на контекст, с ними ассоциированный, и «финалайзер», то есть завершающую функцию. Для более подробной информации можно посмотреть на man dispatch_object.
Очереди посылок
Фундаментальная концепция в GCD — очередь посылок. Это объект, который получает информацию о заданиях и который выполняет их в той же последовательности, в которой получил. Очередь может быть либо параллельной либо последовательной. Параллельная очередь может исполнять много заданий одновременно, насколько это адекватно текущей загрузке системы, примерно как и NSOperationQueue. Последовательная очередь будет выполнять по одной задаче единовременно.
Всего в GCD три типа очередей:
- Главная очередь. Похож на главный поток. На самом деле, задания, которые отправлены на исполнение в главную очередь, исполняются в главном потоке процесса. Ссылку на главную очередь можно получить, вызвав функцию dispatch_get_main_queue(). Так как главная очередь зависима от одного главного потока, это последовательная очередь.
- Глобальные очереди. Глобальные очереди — это параллельные очереди, которые совместно используются всем процессом. Всего их три: очередь с низким приоритетом, очередь с обычным приоритетом, очередь с высоким приоритетом. Ссылки на глобальные очереди можно получить при помощи функций dispatch_get_global_queue, указывая, очередь с каким приоритетом вам нужна.
- Пользовательские (custom) очереди. На самом деле у них нет названия, поэтому будем называть их «пользовательскими». Эти очереди создаются при помощи функции dispatch_queue_create. Это последовательные очереди, которые могут исполнять одно задание в каждый момент времени. Благодаря этому они могут быть как механизм синхронизации, примерно как mutex в традиционном многопоточном программировании.
Создание очередей
Если вы хотите использовать пользовательскую очередь, ее предварительно нужно создать. Для этого просто вызовите функцию dispatch_queue_create. Первый параметр — метка, которая используется исключительно в целях отладки. Apple рекомендует использовать «обратное DNS» именование для создания уникальных имен, например, «com.yourcompany.subsystem.task». Эти имена будут отображаться в протоколах ошибок при сбое и могут быть получены из отладчика, что очень помогает понять, что пошло не так. Второй аргумент функции не поддерживается и в настоящий момент туда нужно передавать NULL.
Передача заданий в очередь
Передача задания в очередь очень проста. Нужно вызвать функцию dispatch_async и передать ей очередь и блок. Очередь выполнит блок, когда сможет. Вот пример исполнения длительной фоновой работы в глобальной очереди:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self goDoSomethingLongAndInvolved];
NSLog(@"Done doing something long and involved");
});
dispatch_async возвращает управление сразу же, а блок будет продолжать выполнение асинхронно в фоне.
Конечно, вызывать NSLog после окончания работы блока не интересно. Обычно вам нужно обновить что-то в Cocoa-интерфейсе, то есть выполнить какой-то код в главном потоке. Это можно сделать, используя наследуемые посылки, то есть посылку в главную очередь внутри задачи, выполняемой в фоновой очереди. Примерно вот так:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self goDoSomethingLongAndInvolved];
dispatch_async(dispatch_get_main_queue(), ^{
[textField setStringValue:@"Done doing something long and involved"];
});
});
Также есть функция dispatch_sync, которая делает то же самое, но при этом ожидая, пока блок завершит исполнение перед тем, как вернуть управление. Вместе с типом __block, это может быть использовано для получения возвращаемого значения из выполненного блока. Например, если у вас есть какой-то код, исполняющийся в фоновом потоке (или, еще лучше сказать, не главной очереди посылок), который должен получить значение из GUI-контрола. Это может быть легко сделано при помощи функций dispatch_sync и dispatch_get_main_queue:
__block NSString *stringValue;
dispatch_sync(dispatch_get_main_queue(), ^{
// __block variables aren't automatically retained
// so we'd better make sure we have a reference we can keep
stringValue = [[textField stringValue] copy];
});
[stringValue autorelease];
// use stringValue in the background now
Правда, возможно, лучше все-таки использовать асинхронный стиль написания кода. То есть вместо синхронного вызова, передать посылку в главную очередь, которая, после завершения, передаст посылку обратно в фоновую очередь. Код будет примерно вот такой:
dispatch_queue_t bgQueue = myQueue;
dispatch_async(dispatch_get_main_queue(), ^{
NSString *stringValue = [[[textField stringValue] copy] autorelease];
dispatch_async(bgQueue, ^{
// use stringValue in the background now
});
});
В зависимости от того, что вам требуется, myQueue может быть либо пользовательской очередью или одной из глобальных очередей.
Замена блокировок
Пользовательские очереди могут быть использованы как механизм синхронизации вместо блокировок (locks). В традиционном многопоточном программировании, если вам необходимо было создавать специальные объекты, которые предназначались для использования в нескольких потоках, создавались специальные блокировки, при помощи которых можно было защитить доступ к такому общему объекту. Псевдокод приведен ниже:
- (id)something {
id localSomething;
[lock lock];
localSomething = [[something retain] autorelease];
[lock unlock];
return localSomething;
}
- (void)setSomething:(id)newSomething {
[lock lock];
if(newSomething != something) {
[something release];
something = [newSomething retain];
[self updateSomethingCaches];
}
[lock unlock];
}
В GCD переменную lock можно замененить на локальную переменную типа «очередь»: dispatch_queue_t queue;. Чтобы использовать очередь как механизм синхронизации, очередь должна быть пользовательской, а не глобальной, чтобы можно было ее проинициализировать при помощи dispatch_queue_create. После этого можно весь код доступа к общим данным внутри dispatch_sync или dispatch_async:
- (id)something {
__block id localSomething;
dispatch_sync(queue, ^{
localSomething = [something retain];
});
return [localSomething autorelease];
}
- (void)setSomething:(id)newSomething {
dispatch_async(queue, ^{
if(newSomething != something)
{
[something release];
something = [newSomething retain];
[self updateSomethingCaches];
}
});
}
Стоит отметить, что очереди посылок исключительно легковесны, поэтому использование их в качестве механизма блокировки полностью оправдано.
В этом месте вы, должно быть, думаете, что это все хорошо, но в чем смысл? Мы перешли с одного механизма на другой, который выглядит примерно также. Зачем? Достоинств у механизма GCD несколько:
- Параллелизм. Посмотрите, как -setSomething использует dispatch_async во второй версии кода. Это обозначает, что вызов из -setSomething тут же вернет управление и вся остальная работа будет выполнена в фоне. Это может быть существенным выигрышем, если updateSomethingCaches — затратная операция, в то время, как вызывающая функция тоже должна будет выполнить что-то объемное.
- Безопасность. Невозможно случайно написать код, который неразблокирует объект, предварительно его заблокировав. В традиционном подходе достаточно легко, например, поставить какой-нибудь return внутрь метода, тем самым оставив блокировку в «подвешенном» состоянии.
- Контроль. Возможность приостановить и возобновить выполнение заданий в очередях отсутствует при использовании традиционных блокировок. Также отсутствует возможность контролировать приоритет выполнения задач «внутри» блокировок и исполнения там же кода в других потоках (например, в главном потоке, если это необходимо).
- Интеграция. Событийная система GCD интегрируется с очередями событий. Любые события или таймеры, которые нужны объекту, могут быть перенаправлены в очередь этого объекта, таким образом заставив их там выполняться, то есть, автоматически «синхронизируясь» с объектом.
Заключение
Теперь вам известны основы GCD, процедура создания очередей, как отправлять задания в очереди и как их использовать для синхронизации вместо многопоточных блокировок. В следующей части будут показаны техники использования GCD для увеличения производительности многоядерных систем. А потом займемся еще более глубоким анализом GCD, включая систему событий и планирование очередей.
Ресурсы
Об авторе оригинальной статьи. Его зовут Mike Ash (Майк Эш), он — разработчик под Mac, как он говорит сам на своей домашней страничке, работает на Rogue Amoeba ночью и летает на глайдере днем.
- Оригинал статьи: Intro to Grand Central Dispatch, Part I: Basics and Dispatch Queues
Очереди посылок -> Глобальные очереди: опечатка в предложении «всего их три: очередь с высоким приоритетом, очередь с высоким приоритетом, очередь с высоким приоритетом.»
Спасибо, поправил.