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

Автор оригинала: Thomas @tclementdev
  • Перевод
(Прим.перев.: автор оригинального материала — пользователь github и twitter Thomas @tclementdev. Ниже в переводе сохранено повествование от первого лица, которое использует автор.)

Думаю, что большинство разработчиков использует libdispatch неэффективно из-за того как её представили сообществу, а также из-за запутанной документации и API. Я пришел к этой мысли после чтения обсуждения «concurrency» в рассылке посвященной развитию Swift (swift-evolution). Особенно просвещают сообщения от Пьера Хабузит (Pierre Habouzit — занимается поддержкой libdispatch в Apple):


Также у него есть много твитов по данной теме:


Вынесенное мной:

  • В программе должно быть очень мало очередей использующих глобальный пул (потоков — прим. пер.). Если все эти очереди будут одновременно активны, то Вы получите такое же количество одновременно выполняющихся потоков. Эти очереди должны рассматриваться как контексты исполнения в программе (GUI, хранилище, работа в фоне, ...), которые получают выгоду от параллельного исполнения.
  • Начните с последовательного исполнения. Когда обнаружите проблему с производительностью, сделайте измерения, чтобы выяснить причину. И если параллельное исполнение помогает, осторожно используйте его. Всегда проверяйте работу параллельного кода под давлением со стороны системы. По умолчанию переиспользуйте очереди. Добавляйте очереди тогда, когда это приносит измеряемые преимущества. В большинстве приложений не стоит использовать более трех-четырех очередей.
  • Очереди, у которых в качестве целевой установлена другая очередь, работают хорошо и масштабируются.
    (Прим. перев.: про установку очереди в качестве целевой для другой очереди можно почитать, например, здесь.)
  • Не используйте dispatch_get_global_queue(). Это не сочетается с качеством обслуживания и приоритетами и может вести к взрывному росту количества потоков. Вместо этого запускайте свой код в одном из своих контекстов исполнения.
  • dispatch_async() является пустой тратой ресурсов, для маленьких исполняемых блоков (< 1 мс), так как этот вызов скорее всего потребует создания нового потока из-за чрезмерного усердия libdispatch. Вместо переключения контекста исполнения для защиты разделяемого состояния используйте механизмы блокировки (lock) одновременного доступа к разделяемому состоянию.
  • Некоторые классы/библиотеки хорошо спроектированы в том отношении, что они переиспользуют контекст исполнения, который им передает вызывающий код. Это позволяет использовать обычную блокировку для обеспечения потокобезопасности. os_unfair_lock — как правило самый быстрый механизм блокировки в системе: лучше работает с приоритетами и вызывает меньше переключений контекста.
  • В случае параллельного исполнения ваши задачи не должны бороться между собой, в противном случае производительность резко падает. Борьба принимает разные формы. Очевидный случай: борьба за захват блокировки. Но в реальности, такая борьба означает ничто иное, как использование разделяемого ресурса, которое становится узким местом: IPC (межпроцессное взаимодействие) / демоны ОС, malloc (блокировка), разделяемая память, ввод / вывод.
  • Вам не нужно, чтобы весь код исполнялся асинхронно для того, чтобы избежать взрывного роста количества потоков. Гораздо лучше использование ограниченного количества нижних очередей и отказ от использования dispatch_get_global_queue().
    (Прим. перев.1: речь, видимо, идёт о случае, когда взрывной рост количества потоков возникает, при синхронизации большого количества параллельно исполняющихся задач «If I have lots of blocks and they all want to wait, we can get what we call thread explosion.»)
    (Прим. перев.2: по обсуждению можно понять, что под нижними очередями Пьер Хабузит подразумевает очереди «которые известны ядру, когда в них есть задачи». Здесь речь идет о ядре ОС.)
  • Нельзя забывать о сложности и багах, которые возникают в архитектуре наполненной асинхронным исполнением и колбэками. Последовательно исполняемый код по прежнему гораздо легче читать, писать и поддерживать.
  • Конкурентные очереди менее оптимизированы, чем последовательные. Используйте их, если Вы измеряете прирост производительности, в противном случае это преждевременная оптимизация.
  • Если Вам нужно отправлять задачи в одну очередь и асинхронно, и синхронно, то вместо dispatch_sync() используйте dispatch_async_and_wait(). dispatch_async_and_wait() не гарантирует исполнение на потоке, с которого произошел вызов, что позволяет уменьшить переключения контекста, когда целевая очередь активна.
    (Прим. перев. 1: на самом деле dispatch_sync() тоже не гарантирует, в документации про него утверждается лишь «исполняет блок на текущем потоке, всегда когда возможно. С одним исключением: блок отправленный в главную очередь — всегда исполняется на главном потоке.»)
    (Прим. перев. 2: о dispatch_async_and_wait() в документации и в исходном коде)
  • Правильно использовать 3-4 ядра не так-то просто. Большинство тех, кто пытается, в действительности не справляются с масштабированием и попусту растрачивают энергию ради крохотного прироста производительности. То, как процессоры работают с перегревом, делу не поможет. Например, Intel отключит Turbo-Boost, если использовать достаточное количество ядер.
  • Измеряйте производительность Вашего продукта в реальном мире, чтобы убедиться, что Вы делаете его быстрее, а не медленнее. Будьте осторожны с микро тестами производительности — они скрывают влияние кэша и держат разогретым пул потоков. Для проверки того, что Вы делаете всегда следует иметь макро тест.
  • libdispatch эффективна, но чудес не бывает. Ресурсы не бесконечны. Вы не можете игнорировать реальность ОС и аппаратного обеспечения, на которых исполняется код. Также не всякий код хорошо распараллеливается.

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

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

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

Еще ссылки


В программе должно быть очень мало очередей, использующих глобальный пул


(Прим. перев.: читая последнюю ссылку, не удержался и перевел кусок из середины переписки Пьера Хабузит с Крисом Латтнером. Ниже один из ответов Пьера Хабузит в 039420.html)
<...>
Я понимаю, что мне тяжело донести свою точку зрения, потому что я — не парень по архитектуре языка, я — парень по системной архитектуре. И я определенно недостаточно понимаю Акторы, чтобы решить как интегрировать их в ОС. Но для меня, возвращаясь к примеру с базой данных, Актор-Базы-Данных, или Актор-Сетевого-Интерфейса из более ранней переписки, являются отличными от, скажем, данного SQL-запроса или данного сетевого запроса. Первые — это сущности о которых ОС должна знать в ядре. В то время как SQL-запрос или сетевой запрос — это всего лишь акторы поставленные в очередь исполнения на первых. Другими словами, эти акторы верхнего уровня отличны потому, что они верхнего уровня, прямо сверху ядра/низкоуровневого рантайма. И это сущность, о которой ядро должно быть способно рассуждать. Это делает их отличными.

В библиотеке dispatch есть 2 вида очередей и соответствующих им уровня API:
  • глобальные очереди, которые не являются очередями подобными другим. И в действительности они — это только абстракция над пулом потоков.
  • все остальные очереди, которые Вы можете устанавливать целевыми одна для другой как захотите.

На сегодняшний день стало ясно, что это была ошибка и что должно быть 3 вида очередей:

  • глобальные очереди, которые не являются настоящими очередями, но представляют, то какое семейство системных атрибутов требует Ваш контекст исполнения (в основном приоритеты). И мы должны запретить отправку задач непосредственно в эти очереди.
  • нижние очереди (которые GCD в последние годы отслеживает и называет «базами» в исходном коде (похоже, имеется в виду исходный код самой GCD — прим. перев.). Нижние очереди известны ядру, когда в них есть задачи.
  • любые другие «внутренние» очереди, о которых ядро вообще не знает.

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

Мне нравится называть вторую группу «контекстами исполнения», но я могу понять почему Вы хотите назвать их Акторами. Это, возможно, более единообразно (и GCD поступила таким же образом, представив и то, и то как очереди). Такие верхнеуровневые «Акторы» должны быть немногочисленны потому, что если они все станут активными одновременно, то им будет нужно такое же количество потоков в процессе. И это не тот ресурс, который можно масштабировать. Вот почему важно различать их. И, как мы и обсуждаем, они также обычно используются для защиты разделяемого состояния, ресурса или чего-то подобного. Сделать это используя внутренние акторы, возможно, не получится.
<...>


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


Не используйте глобальные очереди


Опасайтесь конкурентных очередей


Не используйте async-вызовы для защиты разделяемого состояния


Не используйте async-вызовы для маленьких задач


Некоторые классы/библиотеки должны просто быть синхронными


Борьба параллельных задач между собой — это убийца производительности


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


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


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


Избегайте микро тестов производительности


Ресурсы не безграничны


О dispatch_async_and_wait()


Использование 3-4 ядер — это не что-то простое


Множество улучшений производительности в iOS 12 были достигнуты благодаря однопоточным демонам

  • +14
  • 1,4k
  • 2
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 2

    +2
    Очень странно видеть отрицательную оценку к посту, который является переводом и не видеть комментариев поясняющих суть минусов. Перевод адекватный, автору — большое спасибо.
      0
      Спасибо, приятно получить положительную оценку. Мне стоило добавить примечание в начало. Немного поправил.

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

    Самое читаемое