Миграция далеко от потоков

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

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

Замена потоков с очередями отгрузки

Чтобы понять, как Вы могли бы заменить потоки очередями отгрузки, сначала рассмотрите некоторые способы, которыми Вы могли бы использовать потоки в своем приложении сегодня:

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

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

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

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

Устранение основанного на блокировке кода

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

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

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

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

Реализация асинхронной блокировки

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

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

Перечисление 5-1  , Изменяющее защищенные ресурсы асинхронно

dispatch_async(obj->serial_queue, ^{
   // Critical section
});

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

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

Перечисление 5-2  , Выполняющее критические разделы синхронно

dispatch_sync(my_queue, ^{
   // Critical section
});

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

Изменение к лучшему кода цикла

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

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

Перечисление 5-3 показывает, как заменить a for цикл с его основанным на отгрузке эквивалентом. Блок или функция Вы передаете dispatch_apply или dispatch_apply_f должен принять целочисленное значение, указывающее текущую итерацию цикла. В этом примере код просто распечатывает текущее число цикла к консоли.

Перечисление 5-3  , заменяющее a for цикл без ходьбы

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

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

Простой способ увеличить объем работы в каждой итерации цикла состоит в том, чтобы использовать ходьбу. С ходьбой Вы переписываете свой блочный код для выполнения больше чем одной итерации исходного цикла. Вы тогда сокращаете значение количества, которое Вы указываете к dispatch_apply функция пропорциональной суммой. Перечисление 5-4 показывает, как Вы могли бы реализовать ходьбу для кода цикла, показанного в Перечислении 5-3. В Перечислении 5-4 блок вызывает printf оператор то же число раз как значение шага, которое в этом случае равняется 137. (Фактическое значение шага - что-то, что необходимо сконфигурировать на основе работы, сделанной кодом.), Поскольку существует остаток, перенесенный при делении общего количества итераций значением шага, любые остающиеся итерации выполняются встроенные.

Перечисление 5-4  , Добавляющее шаг к диспетчеризированному для цикла

int stride = 137;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
 
dispatch_apply(count / stride, queue, ^(size_t idx){
    size_t j = idx * stride;
    size_t j_stop = j + stride;
    do {
       printf("%u\n", (unsigned int)j++);
    }while (j < j_stop);
});
 
size_t i;
for (i = count - (count % stride); i < count; i++)
   printf("%u\n", (unsigned int)i);

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

Замена соединений потока

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

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

Для использования группы отгрузки для выполнения той же работы, выполняемой joinable потоками, Вы сделали бы следующее:

  1. Создайте новую группу отгрузки, использующую dispatch_group_create функция.

  2. Добавьте задачи к группе, использующей dispatch_group_async или dispatch_group_async_f функция. Каждая задача, которую Вы представляете группе, представляет работу, которую Вы обычно выполняли бы на joinable потоке.

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

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

Для примера того, как использовать группы отгрузки, посмотрите Ожидание на Группах Задач С очередями. Для получения информации об установке зависимостей между объектами операции посмотрите Зависимости от Взаимодействия Конфигурирования.

Изменение реализаций производителя-потребителя

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

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

  1. Заблокируйте взаимное исключение, связанное с условием (использование pthread_mutex_lock).

  2. Произведите ресурс или работу, которая будет использована.

  3. Сигнализируйте условную переменную, что существует что-то для потребления (использование pthread_cond_signal)

  4. Разблокируйте взаимное исключение (использование pthread_mutex_unlock).

В свою очередь, соответствующий потребительский поток делает следующее:

  1. Заблокируйте взаимное исключение, связанное с условием (использование pthread_mutex_lock).

  2. Установленный a while цикл, чтобы сделать следующее:

    1. Проверьте, чтобы видеть, существует ли действительно работа, которая будет сделана.

    2. Если нет никакой работы, чтобы сделать (или никакой доступный ресурс), вызвать pthread_cond_wait для блокирования текущего потока до, соответствующий сигнал происходит.

  3. Получите работу (или ресурс) предоставленный производителем.

  4. Разблокируйте взаимное исключение (использование pthread_mutex_unlock).

  5. Обработайте работу.

С очередями отгрузки можно упростить реализации производителя и потребителя в единственный вызов:

dispatch_async(queue, ^{
   // Process a work item.
});

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

Замена семафорного кода

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

Для примера того, как использовать семафоры отгрузки, посмотрите Используя Семафоры Отгрузки для Регулирования Использования Конечных Ресурсов.

Замена кода цикла выполнения

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

dispatch_queue_t myNewRunLoop = dispatch_queue_create("com.apple.MyQueue", NULL);

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

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

Совместимость с потоками POSIX

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

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

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

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

Для получения дополнительной информации о потоках POSIX и функциях, упомянутых в этом разделе, посмотрите pthread страницы справочника.