Параллелизм в среде Cocoa, Mac OS, iOS

Отправка очередей (Dispatch Queues).

Grand Central Dispatch (GCD) отправка очередей является мощным инструментом для выполнения задач. Отправка очереди позволяет выполнить произвольныt блокb кода, как асинхронно так и синхронно по отношению к вызвавшему потоку. Вы можете использовать отправку очереди, чтобы выполнить практически все задачи, которые используются для выполнения в отдельных потоках. Преимущество отправки очередей в том, что они проще в использовании и гораздо более эффективны при выполнении этих задач, чем соответствующий многопоточный код.

Отправка очереди является объектно-подобной структурой, которая управляет задачами, которые представляют ее. Все отправленные очереди выстраиваются в структуру данных по принципу: первым вошел, первым вышел. Таким образом, задачи, добаленные в очередь всегда начинаются в том же порядке, в котором они были добавлены. GCD обеспечивает некоторые отправки очереди за вас автоматически, а другие вы можете создать для конкретных целей. В таблице 3-1 перечислены типы отправки очереди доступные для вашего приложения, и описание как вы их далее используете.

Таблица 3-1 Типы отправки очереди

ТипОписание
SerialПоследовательная очередь (также известная как private dispatch queues) выполняет одну задачу за раз в том порядке, в котором они будут добавлены в очередь. Текущее выполнение задач работает на различных потоках (которые могут меняться от задачи к задаче), которые управляются отправкой очереди. Последовательные очереди часто используются для синхронизации доступа к конкретному ресурсу.

Вы можете создать столько последовательных очередей, сколько вам нужно, и каждая очередь работает одновременно в отношении всех других очередей. Другими словами, если вы создадите четыре последовательных очереди, каждая очередь выполняет только одну задачу за раз но до четырех задач, все еще может выполняться одновременно, по одному от каждой очереди. Для получения информации о том, как создать серийную очередь, см. раздел "Создание Последовательной очереди отправки."

ConcurrentПараллельная очередь (также известный как тип глобальной очереди отправки (global dispatch queue)) выполняет одну или несколько задач одновременно, но задачи все еще начанаются в порядке, в котором они были добавлены в очередь. Текущее выполнение задач, работает на различных потоках, которые находятся в ведении отправки очереди. Точное число задач, выполняющихся в данный момент является переменным и зависит от условий системы.

Вы не можете сами создавать параллельные очереди отправки. Вместо этого, существует три глобальных очереди для одновременного использования в вашем приложении. Для получения дополнительной информации о том, как получить глобальную одновременную очередь см. в разделе "Получение глобальной параллельной очереди отправки."

Main dispatch queueОсновная очередь отправки является глобально доступной последовательной очередью, которая выполняет задачи по основным потоке приложения. Эта очередь работает с основным циклом приложения (если таковой имеется), чтобы чередовать выполнение задач в очереди с выполнением других источников событий, прикрепленных к основному циклу приложения. Поскольку она работает на основном потоке приложения, основная очередь часто используется в качестве ключевой точки синхронизации для приложения.

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

Когда дело доходит до добавления параллелизма в приложение, отправка очередей обеспечивает ряд преимуществ перед потоками. Наиболее прямым преимуществом является простота работы модели программирования очереди. С потоками, вы должны написать код, как для задачи, которую необходимо выполнить, так и для создания и управления потоками сами. Отправка очереди позволит вам сосредоточиться на задаче, которую вы действительно хотите выполнить, не беспокоясь о создании потоков и управлении ими. Вместо этого, система обрабатывает все создания и управления потоками за Вас. Преимущество заключается в том, что система может управлять потоками намного эффективнее, чем любое отдельно взятое приложение когда-либо могло-бы. Система может масштабировать количество потоков динамически на основе имеющихся ресурсов и текущего состояния системы. Кроме того, система, как правило, имеет более быструю возможность для запуска вашей задачи, чем вы, если вы создали поток сами.

Хотя вы можете подумать, что переписывать код для отправки очереди будет трудно, часто легче написать код для отправки очереди, чем писать код для потока. Ключ - написание кода для разработки задач, которые являются автономными и способны работать в асинхронном режиме. (На самом деле это верно и для потоков и для отправки очередей). Однако преимущество отправки очереди заключается в предсказуемости. Если у вас есть две задачи, которые обращаются к одному общему ресурсу, но работают на разных потоках, либо поток может изменить ресурс первого и вам нужно будет использовать блокировку, чтобы обе задачи не изменяли этот ресурс в одно и то же время. При отправке очереди, можно добавить задачу к последовательной очереди отправки, чтобы только одна задача изменяла ресурс в любой момент времени. Этот тип очередей на основе синхронизации является более эффективным, чем блокировки, потому что блокировки всегда требуют дорогую ловушку ядра и оспариваются в неоспоримых случаях, в то время как отправка очереди работает в основном в пространстве процесса приложения и только вызывает его на уровне ядра, когда это абсолютно необходимо.

Хотя будет правильным сказать, что две задачи, работающие в последовательной очереди, не могут работать одновременно, вы должны помнить, что если два потока принимают блокировку в одно и то же время, любой параллелизм, предлагаемый потоками теряется или значительно сокращается. Более того, с потоковой моделью требуется создание двух потоков, которые занимают оба ядра и в пользовательском пространстве памяти. Диспетчер очередей не платит такую цену по выделению памяти для своих потоков и потоки, которые они используют сохраняются занятыми и не блокируются.

Технологии связывания очередей

В дополнение к отправке очереди, Grand Central Dispatch предлагает несколько технологий, которые используют очереди, чтобы помочь управлять кодом. В таблице 3-2 перечислены эти технологии, а также ссылки, где вы можете узнать больше информации о них.

Таблица 3-2 технологии, которые используют отправки очереди

ТехнологияОписание
Dispatch groupsГруппа отправки - способ контролировать набор блоковых объектов для завершения. (Вы можете следить за блоками синхронно или асинхронно в зависимости от ваших потребностей). Группы, служат полезным механизмом синхронизации для кода, который зависит от завершения других задач. Для получения дополнительной информации об использовании групп, см. «Ожидание в очереди группы задач".
Dispatch semaphoresОтправки семафоров похожи на традиционный семафор, но, как правило, более эффективны. Отправка семафоров обращается к ядру, только если вызывающий поток должен быть заблокирован, поскольку семафор недоступен. Если семафор доступен, к ядру не обращается. В качестве примера того, как использовать отправку семафоров, см. раздел «Использование диспетчерских семафоров по регулированию использования ограниченных ресурсов".
Dispatch sourcesОтправка источника генерирует уведомления в ответ на конкретные типы системных событий. Вы можете использовать отправку источников для мониторинга событий, таких как процесс уведомления, сигналы, дескрипторы событий и т.д.. Когда происходит событие, отправка источника посылает, вашей задаче код асинхронно по отношению к указанной очереди отправки на обработку. Для получения дополнительной информации о создании и использовании источников отправки см. в разделе "Отправка источников".

Реализация задач с помощью блоков

Блоковые объекты C-ориентированные языковые функции, которые можно использовать в C, Objective-C и C++ коде. Блоки позволяют легко определить автономные единицы для работы. Хотя они могут показаться сродни указателям на функции, блоки фактически представляют основные структуры данных, которые напоминают объект и создаются и управляются для вас компилятором. Компилятор упаковывает код, который вы предоставляете (вместе со всеми связанными с ним данными) и инкапсулирует его в форму, которая может жить в куче и передаваться по всему приложению.

Одним из основных преимуществ блоков является их способность использовать переменные за пределами своей лексической области. При определении блока внутри функции или метода, блок выступает в качестве традиционного блока кода. Например, блок может считывать значения переменных, определенных в родительской области. Переменные доступные блоку копируются в структуру данных блока в куче так, что блок может получить доступ к ним позже. Если блоки будут добавлены в очередь диспетчера, эти значения, обычно, должны быть оставлены в формате только для чтения. Тем не менее, блоки, которые выполняются синхронно также могут использовать переменные, которые имеют ключевое слово __block добавляемое для возврата данных обратному вызову в области родителя.

Вы объявляете встроенные блоки в коде, используя синтаксис, подобный синтаксису, используемому для указателей на функцию. Основное различие между блоком и указателем на функцию в том, что именю блока предшествует вставка (^) вместо звездочки (*). Как и указатель на функцию, вы можете передать аргументы блоку и получить возвращаемое значение из него. Листинг 3-1 показывает, как объявлять и исполнять блоки синхронно в вашем коде. Переменная aBlock объявляет блок, который принимает один целочисленный параметр и не возвращает никакого значения. Фактически блок объявляется и декларируется в одном месте кода. Последняя строка выполняет блок немедленно, печатая указанное число в стандартный вывод.

Листинг 3-1 Простой пример блока

int x = 123;

int y = 456;

// декларирование и назначение блока

void (^aBlock)(int) = ^(int z) {
    printf("%d %d %d\n", x, y, z);
};

// Выполнение блока

aBlock(789);   // напечатает: 123 456 789

Ниже перечислены некоторые из основных принципов, которые необходимо учитывать при проектировании блоков:

  • Для блоков, которые вы планируете выполнять асинхронно с помощью отправки очереди, можно безопасно захватывать скалярные переменные из функции родителя или метода и использовать их в блоке. Тем не менее, вы не должны пытаться захватить большие структуры или другие указатели на основе переменных, которые выделяются и удаляются для вызывающего контекста. К тому времени, как ваш блок выполняется, память на которую ссылается этот указатель может быть освобождена. Конечно, можно безопасно выделить память (или объект) самостоятельно, вручную в блоке.
  • Отправка очереди копий блоков, которые добавляются к ним, и они высвобождают блоки, когда они закончат выполнение. Другими словами, вам не нужно явно копировать блоки перед добавлением их в очередь.
  • Несмотря на то, что очереди более эффективны, чем сырые потоки в выполнении небольших задач, есть еще накладные расходы на создание блоков и выполнение их в очереди. Если блок делает слишком мало работы, то может быть более эффективнее выполнить его как встроенный, чем отправлять в очередь.
  • Не кэшируйте данные, относящиеся к основному потоку в ожидании, что данные должны быть доступны из другого блока. Если задаче, в одной очереди нужно обмениваться данными, используйте контекстный указатель, направленный в очередь, для хранения данных.

Создание и управление диспетчером очереди

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

Получение глобального параллельного диспетчера очереди

Одновременные очереди отправки полезны, когда у вас есть несколько задач, которые могут выполняться параллельно. Одновременные очереди еще являются очередями потому, что они извлекают элементы задач в последовательности первый вошел, первым вышел. Фактическое число задач, выполненяемых одновременно из очереди в данный момент является переменным и может изменяться динамически, в зависимости от изменения условий в приложении. Многие факторы влияют на количество задач, выполняемых в очереди одновременно, в том числе количество доступных ядер, объем работы, проводимой в рамках других процессов, а также количество приоритетных задач в других последовательных очередях отправки.

Система обеспечивает для каждого приложения три одновременные очереди отправки. Эти очереди являются глобальными для применения и различаются только их уровнем приоритета. Поскольку что они носят глобальный характер, вы не создаете их в явном виде. Вместо этого, вы попросите одну из очередей, используя функцию dispatch_get_global_queue, как показано в следующем примере:

dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

В дополнение к получению одновременной очереди по умолчанию, вы также можете получить очереди с высоким и низким уровнями приоритета, передавая константы DISPATCH_QUEUE_PRIORITY_HIGH и DISPATCH_QUEUE_PRIORITY_LOW в функцию.

Примечание: второй аргумент функции dispatch_get_global_queue зарезервирован для будущего расширения. На данный момент, вы всегда должны передавать 0 для этого аргумента.

Несмотря на отправку очереди объектов со счетчиком ссылок, вам не нужно посылать retain и release глобальным одновременным очередям. Поскольку они являются глобальными для приложения, вызовы retain и release на эти очереди, игнорируются. Таким образом, вам не нужно хранить ссылки на эти очереди. Вы можете просто вызывать dispatch_get_global_queue функцию, когда необходима ссылка на одну из них.

Создание диспетчера последовательной очереди

Последовательные очереди полезны, если вы хотите, чтобы ваши задачи выполнялись в определенном порядке. Последовательная очередь выполняет только одну задачу за один раз и всегда тянет задачи из начала очереди. Вы можете использовать последовательные очереди вместо блокировки для защиты общих ресурсов или изменяемыех структур данных. В отличие от блокировки, последовательная очередь гарантирует, что задачи выполняются в определенном порядке. И на всем протяжении представления ваших задач к последовательной очереди асинхронно, в очереди никогда не может наступить deadlock.

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

Листинг 3-2 показывает шаги, необходимые для создания пользовательской последовательной очереди. Функция dispatch_queue_create принимает два параметра: имя очереди и набор атрибутов очереди. Отладчик и инструменты производительности отображают имя очереди, чтобы помочь вам отслеживать выполнение ваших задач. Атрибуты очереди зарезервированы для будущего использования и должены быть NULL.

Листинг 3-2 Создание новой последовательной очереди

dispatch_queue_t queue;

queue = dispatch_queue_create("com.example.MyQueue", NULL);

Получение общей очереди во время выполнения

Grand Central Dispatch предоставляет функции, которые позволяют получать доступ к нескольким общим очередям отправки из вашего приложения:

  • Используйте функцию dispatch_get_current_queue для отладки целей или для проверки идентичности очереди. Вызов этой функции внутри блокового объекта возвращается в очередь, в которой блок был представлен (и на которой он сейчас предположительно работает). Вызов этой функции за пределами блока возвращает одновременную очередь по умолчанию для вашего приложения.
  • Используйте функцию dispatch_get_main_queue, чтобы получить последовательную очередь отправки, связанную с основным потоком приложения. Эта очередь создается автоматически для Cocoa приложений и для приложений, которые либо вызвали dispatch_main функцию или настроили цикл выполнения (с помощью объекта типа CFRunLoopRef или NSRunLoop) в главном потоке.
  • Используйте dispatch_get_global_queue функцию, чтобы получить любую из общих одновременных очередей. Для получения дополнительной информации см. раздел "Получение глобального параллельного диспетчера очереди."

Управление памятью для отправки очереди

Отправка очереди и другие объекты отправки являются типами данных с подсчетом ссылок. При создании последовательной очереди отправки, она имеет начальное значение счетчика ссылок 1. Вы можете использовать функции dispatch_retain и dispatch_release для увеличения и уменьшения количества ссылок по мере необходимости. Когда счетчик ссылок очереди достигает нуля, система асинхронно освобождает очередь.

Важно сохранять и освобождать объекты отправки, такие как очереди, чтобы гарантировать, что они остаются в памяти, в то время как они используются. Как и в памяти управляемых объектов Cocoa, общее правило заключается в том, что если вы планируете использовать очередь, которая была передана в коде, вы должны вызвать retain к очереди, прежде чем использовать ее и release ее, когда она вам больше не нужна. Эта основная схема гарантирует, что очередь остается в памяти до тех пор, пока вы ее используете.

Примечание: Вам не нужно сохранять или освобождать все глобальные очереди отправки, в том числе одновременные очереди отправки или основную очередь отправки. Любые попытки сохранить или освободить очереди игнорируются.

Даже если вы реализовали сбор мусора приложений, вы все равно должны сохранять и освобождать ваши очереди отправки и другие объекты диспетчеризации. Grand Central Dispatch не поддерживает модель сборки мусора для регенерации памяти.

Хранение пользовательских данных в контексте с очередью

Все объекты отправки (в том числе очереди отправки) позволяют связать пользовательские данные контекста с объектом. Чтобы задать и получить эти данные на данный объект, можно использовать и dispatch_set_context, dispatch_get_context функции. Система не использует пользовательские данные, и в любом случае, это ваша проблема, как выделять и освобождать данные в нужное время.

Для очереди, вы можете использовать контекст данных для хранения указателя на Objective-C объект или другие структуры данных, что помогает определить очередь или его предназначение в вашем коде. Вы можете использовать функцию завершения очереди, чтобы освободить (или разъединить) данные контекста из очереди, прежде чем она будет освобождена. Пример того, как написать функцию завершения, которая очищает контекст очереди данных показан в листинге 3-3.

Предоставление очищающей функции для очереди

После создания последовательной очереди отправки, вы можете прикрепить функцию завершения для выполнения любой пользовательской очистки, когда очередь освобождается. Очередью отправки учитываются ссылки на объекты, и вы можете использовать функцию dispatch_set_finalizer_f для указания функции, которая будет выполняться, когда количество ссылок на вашу очередь дойдет до нуля. Вы можете использовать эту функцию для очистки контекстных данных, связанных с очередью и функцией, вызываемой только в том случае, если указатели на контекст не NULL.

Листинг 3-3 показывает пользовательские функции завершения и функцию, которая создает очередь и устанавливает функцию завершения. Очередь использует функцию завершения для освобождения данных, хранящихся в контексте указатели очереди. (myInitializeDataContextFunction и myCleanUpDataContextFunction функции ссылаются из кода пользовательских функций, которые вы предоставляете для инициализации и очистки данных содержимого самой структуры). Контекстные указатели передается завершающей функции и содержат данные объекта, связанного с очередью.

Листинг 3-3 Установка функции очистки очереди

void myFinalizerFunction(void *context)
{
    MyDataContext* theData = (MyDataContext*)context;

    // Очистка контекста
    myCleanUpDataContextFunction(theData);

    // Освобождение структуры.

    free(theData);

} 

dispatch_queue_t createMyQueue()
{
    MyDataContext*  data = (MyDataContext*) malloc(sizeof(MyDataContext));
    myInitializeDataContextFunction(data);

    // Создание очереди и установки данных контекста
    dispatch_queue_t serialQueue = dispatch_queue_create("com.example.CriticalTaskQueue", NULL);
    if (serialQueue)
    {
        dispatch_set_context(serialQueue, data);
        dispatch_set_finalizer_f(serialQueue, &myFinalizerFunction);
    }
 
    return serialQueue;
}

Добавление задач в очередь

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

Добавление одиночной задачи в очередь

Существуют два способа добавить задачу в очередь: асинхронно или синхронно. Когда это возможно, асинхронное выполнение с использованием dispatch_async и dispatch_async_f функций предпочтительнее, чем синхронный вариант. При добавлении объекта блока или функции в очередь, нет никакого способа узнать, когда этот код будет выполняться. В результате, добавляя блоки или функции асинхронно позволяет запланировать выполнение кода и продолжать делать другую работу из вызывающего потока. Это особенно важно, если вы планировании задачу из основного потока приложения, возможно, в ответ на некоторые пользовательские события.

Хотя вы должны добавлять задачи асинхронно по мере возможности, все же могут быть случаи, когда вам нужно добавить задачу синхронно, чтобы предотвратить гонку условий или другие ошибки синхронизации. В этих случаях можно использовать функции dispatch_sync и dispatch_sync_f для добавления задачи в очередь. Эти функции блокируют текущий поток исполнения до завершения выполнения указанной задачи.

Важно: Вы никогда не должны вызывать функции dispatch_sync или dispatch_sync_f из задачи, которая выполняется в той же очереди, в которой вы планируете переход к функции. Это особенно важно для последовательных очередей, которые гарантированно приведут к deadlock, но также следует избегать одновременных очередей.

Следующий пример показывает, как использовать блочные варианты для отправки задачи асинхронно и синхронно:

dispatch_queue_t myCustomQueue;
myCustomQueue = dispatch_queue_create("com.example.MyCustomQueue", NULL);

dispatch_async(myCustomQueue, ^{
    printf("Сделайте некую работу здесь.\n");
});

 

printf("Первый блок может работать или может не работать.\n");

 

dispatch_sync(myCustomQueue, ^{

    printf("Сделайте еще некую работу здесь.\n");

});

printf("Оба блока были завершены.\n");

Выполнение завершающего блока когда задача выполнена

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

Завершающий блок это просто еще один кусок кода, который вы запускаете в очереди в конце исходной задачи. Вызывающий код как правило, обеспечивает завершающий блок в качестве параметра при запуске задачи. Все что код должен сделать, это представить указанный блок или функцию в указанной очереди, когда она завершит свою работу.

Листинг 3-4 показывает функцию усреднения реализованнную с использованием блоков. Последние два параметра функции усреднения позволяют вызывающему коду, задавшему очередь и блок, использовать при предоставлении результатов. После того как функция усреднения вычисляет значение, она передает результаты на указанный блок и передает его в очередь. Во избежание преждевременного высвобождения очереди, очень важно сохранить эту очередь сначала и высвободить ее после завершения блока отправки.

Листинг 3-4 Выполнение завершения обратного вызова после того, как задача завершена

void average_async(int *data, size_t len, dispatch_queue_t queue, void (^block)(int))
{
   //Сохраните очередь, предоставленной пользователем, чтобы
   //она не исчезла до завершения
   // вызова блока

   dispatch_retain(queue);

   // Сделайте работу в одновременной очереди по умолчанию, а затем
   // предоставленный пользователем блок на выполнение

   dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      int avg = average(data, len);
      dispatch_async(queue, ^{ block(avg);});

      // Освободите предоставленные пользователем очереди, после завершения

      dispatch_release(queue);
   });
}

Выполнение итераций цикла одновременно

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

for (i = 0; i < count; i++) {

   printf("%u\n",i);

}

Если работа выполняемая во время каждой итерации отличается от работы, выполненняемой во время всех других итераций, и порядок, в котором каждый последующий цикл заканчивается не имеет значения, вы можете заменить цикл вызовом dispatch_apply или dispatch_apply_f функции. Эти функции посылают указанный блок или функцию в очередь для каждой итерации. Поскольку отправка происходит в одновременную очередь, возможно выполнять несколько итераций в одно и то же время.

Вы можете указать либо последовательную либо одновременную очередь при вызове dispatch_apply или dispatch_apply_f. Передача в параллельную очередь позволяет выполнять несколько итераций одновременно и является наиболее распространенным способом использования этих функций. Хотя использование последовательной очереди допустимо и правильно выполняет Ваш код, ее использование не имеет реального преимущества в производительности над исходным простым циклом.

Листинг 3-5 показывает, как заменить предыдущий for цикл с синтаксисом dispatch_apply. Блок, переданный в dispatch_apply функцию должен содержать один параметр, который определяет текущую итерацию цикла. Когда блок выполняется, значение этого параметра равно 0 для первой итерации, 1 для второй, и так далее. Значение параметра для последней итерации это count - 1, где count - общее количество итераций.

Листинг 3-5 Одновременное выполнение итераций for цикла

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_apply(count, queue, ^(size_t i) {
   printf("%u\n",i);
});

Прежде, чем посылать любой цикл в очередь на параллельное выполнение, убедитесь, что каждая итерация делает хоть сколько-нибудь значимую работу, поскольку издержки на диспетчирезацию очереди могут перекрыть всю пользу от параллелизма, а возможно и замедлить выполнение относительно простого for цикла. Возможно следует объединить несколько итераций в одну.

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

Grand Central Dispatch обеспечивает специальную очередь отправки, которую можно использовать для выполнения задач на основном потоке приложения. Эта очередь предоставляется автоматически для всех приложений и автоматически освобождается, любое приложение, которое создает цикл выполнения (управляемый либо CFRunLoopRef или объектом типа NSRunLoop) на основном потоке. Если вы не создаете Cocoa приложения и не хотите, чтобы создался цикл выполнения явно, необходимо вызвать функцию dispatch_main для отвода основной очереди отправки в явном виде. Вы все еще можете добавлять задачи в очередь, но если вы не вызываете эту функцию то задачи, никогда не выполняются.

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

Использование Objective-C объектов в Ваших задачах

GCD обеспечивает встроенную поддержку для методов управления памятью Cocoa, так что вы можете свободно использовать Objective-C объекты в блоках, которые Вы направили в очереди. Каждая очередь отправки поддерживает свой собственный autorelease пул, чтобы autoreleased объекты освобождались в какой-то момент, хотя, очереди не дают никаких гарантий относительно того, когда они на самом деле освободят эти объекты. В сборщике мусора приложений, GCD также регистрирует каждый поток создаваемый в системе сбора мусора.

Если ваше приложение ограничено ресурсами памяти и ваш блок создает более чем несколько autoreleased объектов, создание своего собственного autorelease пула является единственным способом гарантировать, что ваши объекты освобождаются своевременно. Как правило, создают NSAutoreleasePool объект в начале вашего блока кода и сливают его (или освобождают пул) в конце вашего блока кода. Если ваш блок создает сотни объектов, вы можете создать более одного autorelease пула или слить ваш пул на регулярной основе.

Для получения дополнительной информации о autorelease пулах и Objective-C управлении памятью, см. в разделе "Управление памятью в Cocoa".

Приостановка и возобновление очереди

Можно запретить очереди исполнение блока объектов, временно приостановив его. Вы приостанавливаете очередь отправки с помощью функции dispatch_suspend и возобновлякте ее с помощью функции dispatch_resume. Вызов dispatch_suspend наращивает значение счетчика приостоновки на 1, а вызов dispatch_resume уменьшает счетчик приостоновок. В то время как счетчик приостановки больше нуля, очередь остается приостановленной. Таким образом, необходимо сбалансировать все вызовы приостановки с соответствующими вызовами возобновления для возобновления обработки блоков.

Важно! Вызовы приостановить и возобновить асинхронны и вступают в силу только между началом выполнения блоков. Приостановка очереди не произойдет для уже выполняющегося блока и он закончит свое выполнение.

Использование диспетчерских семафоров по регулированию использования ограниченных ресурсов

Если задачи, которые вы представляете очереди отправки получают доступ к некоторым ограниченным ресурсам, вы можете использовать семафор отправки для регулирования количества задач одновременно доступных этому ресурсу. Семафор отправкиа работает как обычный семафор с одним исключением. При наличии ресурсов, он занимает меньше времени, чтобы осуществить отправку семафора, чем при использовании традиционных системных семафоров. Это происходит потому, Grand Central Dispatch не обращается в ядру ​​в данном конкретном случае. Единственный раз, когда он спускается до уровня ядра, это когда ресурс недоступен, и система должна оставить свой поток, пока семафор сигналит.

Семантика для использования семафора отправки заключается в следующем:

  1. При создании семафора (с помощью функции dispatch_semaphore_create), можно указать положительное число, показывающее количество ресурсов.
  2. В каждой задаче, вызовите dispatch_semaphore_wait, для ожидания семафора.
  3. Когда вызов ожидания возвращается, приобретйте ресурсы и делайте свою работу.
  4. Когда вы закончите работу с ресурсом, высвободите его и сигнал семафора, вызовом функции dispatch_semaphore_signal.

В качестве примера того, как эти шаги работают, рассмотрим вопрос об использовании файловых дескрипторов в системе. Каждое приложение получает ограниченное число дескрипторов файлов для использования. Если у вас есть задача, которая обрабатывает большое количество файлов, и вы не хотите открывать так много файлов за один раз при запуске из файловых дескрипторов. Вместо этого, вы можете использовать семафор, чтобы ограничить число файловых дескрипторов для одновременного использования Вашим кодом обработки файлов. Основные фрагменты кода, которые вы бы включили в свои задачи выглядят следующим образом:

// Создание семафора, с указанием начального размера пула

dispatch_semaphore_t fd_sema = dispatch_semaphore_create(getdtablesize() / 2);

// Ожидаем освобождения файлового дескриптора

dispatch_semaphore_wait(fd_sema, DISPATCH_TIME_FOREVER);
fd = open("/etc/services", O_RDONLY);

// Освобождаем файловый дескриптор после окончания работы с ним

close(fd);
dispatch_semaphore_signal(fd_sema);

При создании семафора, необходимо указать количество имеющихся ресурсов. Это значение становится начальным значением переменной счетчика семафора. Каждый раз, когда вы ожидаете от семафора, функция dispatch_semaphore_wait уменьшает значение переменной на 1. Если в результате получается отрицательное значение, функция сообщает ядру, чтобы оно заблокировало ваш поток. С другой стороны, функция dispatch_semaphore_signal приращивает значение переменной на 1, чтобы указать, что ресурс был освобожден. Если есть задачи заблокированые в ожидании ресурсов, одна из них впоследствии разблокируется и позволит выполнить свою работу.

Ожидание в очереди группы задач

Групповая отправка является одним из способов, чтобы блокировать поток с одной или несколькими задачами завершить выполнение. Вы можете использовать это поведение в местах, где вы не можете добиться прогресса, пока все указанные задачи являются полными. Например, после отправки нескольких задач для вычисления некоторых данных, вы можете использовать группу для ождания результатов от тех задач, а затем обработать результаты, когда они сделаны. Другой способ использования групп отправки в качестве альтернативы потоковым соединениям. Вместо того, чтобы начать несколько дочерних потоков, а затем присоединиться к каждому из них, вы можете добавить соответствующие задачи в группу отправки и ождать всю группу.

Листинг 3-6 показывает основной процесс создания группы, диспетчеризации задач, и ожидания результатов. Вместо того, чтобы диспетчеризировать задачи в очереди, используя dispatch_async функции, вместо этого можно использовать функцию dispatch_group_async. Эта функция связывает задачу с группой и очередью исполнения. Подождать выполнения группы задач до конца, используя функцию dispatch_group_wait, переходя в соответствующую группу.

Листинг 3-6 Ожидание асинхронных задач

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();

// Добавляем задачу к группе

dispatch_group_async(group, queue, ^{

   // Какая либо асинхронная работа
});

// Выполняем какую либо работу пока задача выполняется

// Когда мработа не может двигаться дальше без результатов
// работы задач из очереди, ожидаем их, блокируя поток

dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

// освобождаем группу, после ее ненадобности

dispatch_release(group);

Очереди отправки и Потокобезопасность

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

  • Очереди отправки сами являются потокобезопасными. Другими словами, вы можете представить задачи очереди отправки из любого потока в системе, предварительно не разблокируя или синхронизируя доступ к очереди.
  • Не вызывайте dispatch_sync функцию из задачи, которая выполняется на той же очереди которой вы передаете для вызова функцию. Это будет deadlock очереди. Если Вам необходимо направить в текущую очередь, сделайте это асинхронно с помощью функции dispatch_async.
  • Избегайте блокировок из задач, которые вы послали отправке очереди. Несмотря на то, можно безопасно использовать блокирование из Ваших задач, когда вы используете блокировку, вы рискуете заблокировать последовательную очередь полностью, даже если это блокировка недоступна. Аналогично, для одновременной очереди, ожидание блокировки может предотвратить другие задачи. Если вам нужно синхронизировать части кода, используйте последовательные очереди отправки, а не блокировку.
close

Блок объект Читать более подробно

Блок объекты - синтаксические и выполняемые функций C-уровня, которые позволяют составить функцию выражения, которая может быть переданы в качестве аргумента, при необходимости хранения и использования в многопоточных приложениях. Функция выражения может ссылаться и может сохранить доступ к локальным переменным. В других языках и средах, блок объекта иногда называют закрытием или лямбдой. Вы можете использовать блок, когда вы хотите создать рабочие модули (то есть сегменты кода), которые могут быть переданы как будто они являются значениями. Блоки предлагают более гибкое программирование и больше возможностей. Вы можете использовать их, например, для написания обратных вызовов или для выполнения операции для всех элементов в коллекции.

Объявление блока

Во многих случаях, можно использовать встроенные блоки, поэтому вам не нужно объявлять их. Синтаксиса декларации, однако, похож на стандартный синтаксис указателей на функции, за исключением того, что вы используете вставки (^) вместо звездочки указателя (*). Например, следующий код объявляет переменную aBlock, который ссылается на блок, который требует трех параметров и возвращает значение с плавающей точкой:

float (^aBlock)(const int*, int, float);

Создание блока

Вы можете использовать вставки (^) оператора для обозначения начала и точку с запятой, чтобы обозначить конец блока выражения. В следующем примере объявляется простой блок и относит его к ранее объявленной блоком переменной (oneFrom):

int (^oneFrom)(int);

oneFrom = ^(int anInt) {
    return anInt - 1;
};

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

Если вы явно не объявляете возвращаемое значение выражения блока, оно может автоматически выводится из содержимого блока.

Блок-изменяемые переменные

Вы можете использовать __block модификатор хранения c переменными, объявленными локально с вложениями лексической области, чтобы обозначить, что такие переменные должны быть предоставлены по ссылке в блоке и являются изменяемыми. Любые изменения находят свое отражение в окружающей лексической области, в том числе любых других блоков, определенных в той же ограждающей лексической области.

Использование блоков

Если вы объявляете блок в качестве переменной, вы можете использовать его так же, как функцию. В следующем примере печатается 9.

printf("%d\n", oneFrom(10));

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

В следующем примере определяется, содержит ли объект NSSet слово, указанное в локальной переменной и задает значение другой локальной переменной (found) в YES (и прекращает поиск), если он выполняется. В этом примере, found объявлена ​​как переменная __block.

__block BOOL found = NO;
NSSet *aSet = [NSSet setWithObjects: @"Alpha", @"Beta", @"Gamma", @"X", nil];
NSString *string = @"gamma";

[aSet enumerateObjectsUsingBlock:^(id obj, BOOL *stop) {
    if ([obj localizedCaseInsensitiveCompare:string] == NSOrderedSame) {
        *stop = YES;
        found = YES;
    }
}];

// В этой точке, found == YES

В этом примере блок, содержащийся в списке аргументов метода. Блок также использует стек локальных переменных.

Операции сравнения

Одной из наиболее распространенных операций, которые вы выполняете с блоками в среде Cocoa сравнение двух объектов, сортировка содержимого массива. Среда выполнения Objective-C определяет блок типа NSComparator для использования этих сравнений.

 
 
homeЗаметили ошибкукарта сайта 
   Made on a Mac