Эффективное Использование Libdispatch

( Примечание перевода: автор оригинального материала — пользователь github И Твиттер Томас @tclementdev. В приведенном ниже переводе сохранено повествование от первого лица, использованное автором.

) Я думаю, что большинство разработчиков используют libdispatch неэффективно из-за того, как он был представлен сообществу, а также из-за запутанной документации и API. Я пришел к этой идее после прочтения обсуждения «параллелизма» в списке рассылки разработчиков Swift (swift-evolution).

Особенно поучительны сообщения Пьера Хабузи (который поддерживает libdispatch в Apple):

У него также есть много твитов на эту тему: Мой вывод:
  • В программе должно быть очень мало очередей, использующих глобальный пул ( потоки - ок.

    переулок ).

    Если все эти очереди активны одновременно, то вы получите одинаковое количество одновременно выполняющихся потоков.

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

  • Начните с последовательного выполнения.

    При обнаружении проблемы с производительностью проведите измерения, чтобы определить причину.

    И если параллельное выполнение помогает, используйте его осторожно.

    Всегда проверяйте работу параллельного кода под давлением системы.

    Повторное использование очередей по умолчанию.

    Добавляйте очереди, когда это приносит измеримую выгоду.

    Большинству приложений не следует использовать более трех или четырех очередей.

  • Очереди, целью которых является другая очередь, работают хорошо и масштабируются.

    ( Перевод примечания: вы можете прочитать о настройке очереди в качестве цели для другой очереди, например, Здесь .

    )

  • Не используйте send_get_global_queue().

    Это не соответствует качеству обслуживания и приоритетам и может привести к резкому увеличению количества потоков.

    Вместо этого запустите свой код в одном из контекстов выполнения.

  • send_async() — это пустая трата ресурсов на маленькие исполняемые блоки (< 1ms), since this call will most likely require the creation of a new thread due to libdispatch being overzealous. Instead of switching execution context to protect shared state, use mechanisms to lock concurrent access to shared state.
  • Некоторые классы/библиотеки хорошо спроектированы, поскольку они повторно используют контекст выполнения, передаваемый им вызывающим кодом.

    Это позволяет использовать регулярную блокировку для обеспечения безопасности резьбы.

    os_unfair_lock обычно является самым быстрым механизмом блокировки в системе: он лучше обрабатывает приоритеты и вызывает меньшее количество переключений контекста.

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

    Очевидный случай: борьба за захват замка.

    Но на самом деле такая борьба означает не что иное, как использование общего ресурса, который становится узким местом: IPC (межпроцессное взаимодействие)/демоны ОС, malloc (блокировка), общая память, ввод-вывод.

  • Вам не обязательно, чтобы весь ваш код выполнялся асинхронно, чтобы избежать взрыва потока.

    Гораздо лучше использовать ограниченное количество нижних очередей и избегать использования send_get_global_queue().

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

    «Если у меня много блоков и все они хотят подождать, мы можем получить то, что мы называем взрывом потоков».

    ) ( Примечание перевода 2: автор обсуждение можно понять, что под меньшими очередями Пьер Хабузи имеет в виду очереди "которые известны ядру, когда они содержат задачи" .

    Здесь речь идет о ядре ОС.

    )

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

    Последовательно выполняемый код по-прежнему гораздо проще читать, писать и поддерживать.

  • Параллельные очереди менее оптимизированы, чем последовательные.

    Используйте их, если измеряете прирост производительности, иначе это преждевременная оптимизация.

  • Если вам нужно отправлять задачи в одну и ту же очередь как асинхронно, так и синхронно, используйте диспетчерскую_async_and_wait() вместо диспетчерской_синхронизации().

    диспетчеризация_async_and_wait() не гарантирует выполнение в потоке, из которого произошел вызов, что помогает уменьшить количество переключений контекста, когда целевая очередь активна.

    ( Обратите внимание на перевод 1: фактически, send_sync() тоже не гарантирует; в документации об этом говорится только «выполняет блок в текущем потоке, когда это возможно».

    За одним исключением: блок, отправленный в основную очередь, всегда выполняется в основном потоке».

    ) ( Примечание к переводу 2: о диспетчеризации_async_and_wait() в документация И в исходном коде )

  • Правильно использовать 3-4 ядра не так уж и просто.

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

    То, как процессоры справляются с перегревом, не поможет. Например, Intel отключит Turbo-Boost, если используется достаточно ядер.

  • Измерьте производительность вашего продукта в реальном мире, чтобы убедиться, что вы делаете его быстрее, а не медленнее.

    Будьте осторожны с микротестами — они скрывают влияние кэша и поддерживают пул потоков в тепле.

    У вас всегда должен быть макро-тест, чтобы проверить, что вы делаете.

  • libdispatch эффективен, но чудес не бывает. Ресурсы не бесконечны.

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

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

В большинстве случаев блокировка, вероятно, является лучшим выбором.

Как только вы начинаете использовать и повторно использовать очереди (контексты выполнения) из заранее разработанного набора, возникает опасность возникновения взаимоблокировок.

Опасность возникает при отправке задач в эти очереди с помощью send_sync().

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

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

Я лично видел огромное улучшение производительности, следуя этим рекомендациям.

(в высоконагруженных программах).

Это новый подход, но оно того стоит.

Больше ссылок

В программе должно быть очень мало очередей при использовании глобального пула.

( Примечание перевода: прочитав последнюю ссылку, я не удержался и перевел отрывок из середины переписки Пьера Хабузи с Крисом Латтнером.

Ниже приведен один из ответов Пьера Хабузи на 039420.html. )

<.

> Я понимаю, что мне сложно донести свою точку зрения, потому что я не специалист по языковой архитектуре, я специалист по системной архитектуре.

И я определенно недостаточно разбираюсь в Актерах, чтобы решить, как их интегрировать в ОС.

Но для меня, возвращаясь к примеру с базой данных, субъект базы данных или субъект сетевого интерфейса из предыдущей переписки отличается, скажем, от данного SQL-запроса или данного сетевого запроса.

Первые — это сущности, о которых ОС должна знать в ядре.

В то время как SQL-запрос или сетевой запрос — это просто субъекты, которые сначала помещаются в очередь выполнения.

Другими словами, эти актеры верхнего уровня отличаются тем, что они находятся на верхнем уровне, прямо над средой выполнения ядра/низкого уровня.

И это сущность, о которой ядро должно иметь возможность рассуждать.

Вот что делает их замечательными.

Библиотека диспетчеризации имеет 2 типа очередей и соответствующие им уровни API:

  • глобальные очереди, которые не являются очередями, как другие.

    А на самом деле это всего лишь абстракция пула потоков.

  • все остальные очереди, которые вы можете установить в качестве целей друг для друга по своему желанию.

Сегодня стало понятно, что это была ошибка и что должно быть 3 типа очередей:
  • глобальные очереди, которые не являются настоящими очередями, но представляют, какое семейство системных атрибутов требуется вашему контексту выполнения (в основном приоритеты).

    И мы должны предотвратить отправку задач напрямую в эти очереди.

  • нижние очереди (которые GCD отслеживает и называет «базами» в исходном коде в последние годы ( Похоже, имеется в виду исходный код самого НОД - прим.

    перевод ).

    Нижние очереди известны ядру, когда в них есть задачи.

  • любые другие «внутренние» очереди, о которых ядро вообще не знает.
В команде разработчиков диспетчеризации мы каждый день сожалеем о том, что различие между второй и третьей группами очередей не было четко прописано в API. Мне нравится называть вторую группу «контекстами выполнения», но я понимаю, почему вы хотите называть их Актерами.

Возможно, это более единообразно (и GCD сделал то же самое, представляя и то, и другое в виде очередей).

Таких «Актеров» верхнего уровня должно быть немного, потому что, если они все станут активными одновременно, им потребуется одинаковое количество потоков в процессе.

И это не тот ресурс, который можно масштабировать.

Вот почему важно различать их.

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

Возможно, это невозможно сделать с помощью внутренних актеров.

<.

>

Начните с последовательного выполнения Не используйте глобальные очереди Остерегайтесь конкурентных очередей Не используйте асинхронные вызовы для защиты общего состояния.

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

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

Не используйте семафоры для ожидания асинхронной задачи.

API NSOperation имеет несколько серьезных ошибок, которые могут привести к снижению производительности.

Избегайте микротестов производительности Ресурсы не безграничны О диспетчеризации_async_and_wait() Использовать 3-4 ядра — непростая задача Многие улучшения производительности в iOS 12 были достигнуты благодаря однопоточным демонам.

Теги: #разработка iOS #производительность #Swift #objective-c #CGD
Вместе с данным постом часто просматривают: