(Прим.перев.: автор оригинального материала — пользователь github и twitter Thomas @tclementdev. Ниже в переводе сохранено повествование от первого лица, которое использует автор.)
Думаю, что большинство разработчиков использует libdispatch неэффективно из-за того как её представили сообществу, а также из-за запутанной документации и API. Я пришел к этой мысли после чтения обсуждения «concurrency» в рассылке посвященной развитию Swift (swift-evolution). Особенно просвещают сообщения от Пьера Хабузит (Pierre Habouzit — занимается поддержкой libdispatch в Apple):
Также у него есть много твитов по данной теме:
Вынесенное мной:
Взгляните на все вызовы dispatch_async() в своем коде и спросите себя: действительно ли задача, которую Вы отправляете этим вызовом, стоит переключения контекста. В большинстве случаев, вероятно, блокировка — это лучший выбор.
Как только Вы начнёте использовать и переиспользовать очереди (контексты исполнения) из заранее спроектированного набора, возникнет опасность взаимоблокировок. Опасность возникает при отправлении в эти очереди задач с помощью dispatch_sync(). Обычно это случается, когда очереди используются для потокобезопасности. Поэтому еще раз: решением является применение механизмов блокировки и использование dispatch_async() только, когда нужно переключение в другой контекст исполнения.
Я лично видел огромные улучшения производительности от следования данным рекомендациям
(в высоконагруженных программах). Это новый подход, но он того стоит.
В программе должно быть очень мало очередей, использующих глобальный пул
(Прим. перев.: читая последнюю ссылку, не удержался и перевел кусок из середины переписки Пьера Хабузит с Крисом Латтнером. Ниже один из ответов Пьера Хабузит в 039420.html)
Начните с последовательного исполнения
Не используйте глобальные очереди
Опасайтесь конкурентных очередей
Не используйте async-вызовы для защиты разделяемого состояния
Не используйте async-вызовы для маленьких задач
Некоторые классы/библиотеки должны просто быть синхронными
Борьба параллельных задач между собой — это убийца производительности
Чтоб избежать взаимоблокировок, используйте механизмы блокировки, когда нужно защитить разделяемое состояние
Не используйте семафоры для ожидания асинхронной задачи
API NSOperation имеет несколько серьезных ловушек, которые могут привести к падению производительности
Избегайте микро тестов производительности
Ресурсы не безграничны
О dispatch_async_and_wait()
Использование 3-4 ядер — это не что-то простое
Множество улучшений производительности в iOS 12 были достигнуты благодаря однопоточным демонам
Думаю, что большинство разработчиков использует libdispatch неэффективно из-за того как её представили сообществу, а также из-за запутанной документации и API. Я пришел к этой мысли после чтения обсуждения «concurrency» в рассылке посвященной развитию Swift (swift-evolution). Особенно просвещают сообщения от Пьера Хабузит (Pierre Habouzit — занимается поддержкой libdispatch в Apple):
- lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170828/date.html
- lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170904/date.html
Также у него есть много твитов по данной теме:
Вынесенное мной:
- В программе должно быть очень мало очередей использующих глобальный пул (потоков — прим. пер.). Если все эти очереди будут одновременно активны, то Вы получите такое же количество одновременно выполняющихся потоков. Эти очереди должны рассматриваться как контексты исполнения в программе (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() только, когда нужно переключение в другой контекст исполнения.
Я лично видел огромные улучшения производительности от следования данным рекомендациям
(в высоконагруженных программах). Это новый подход, но он того стоит.
Еще ссылки
В программе должно быть очень мало очередей, использующих глобальный пул
- lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170828/039368.html
- lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170828/039405.html
- lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170828/039410.html
- lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170828/039420.html
(Прим. перев.: читая последнюю ссылку, не удержался и перевел кусок из середины переписки Пьера Хабузит с Крисом Латтнером. Ниже один из ответов Пьера Хабузит в 039420.html)
<...>
Я понимаю, что мне тяжело донести свою точку зрения, потому что я — не парень по архитектуре языка, я — парень по системной архитектуре. И я определенно недостаточно понимаю Акторы, чтобы решить как интегрировать их в ОС. Но для меня, возвращаясь к примеру с базой данных, Актор-Базы-Данных, или Актор-Сетевого-Интерфейса из более ранней переписки, являются отличными от, скажем, данного SQL-запроса или данного сетевого запроса. Первые — это сущности о которых ОС должна знать в ядре. В то время как SQL-запрос или сетевой запрос — это всего лишь акторы поставленные в очередь исполнения на первых. Другими словами, эти акторы верхнего уровня отличны потому, что они верхнего уровня, прямо сверху ядра/низкоуровневого рантайма. И это сущность, о которой ядро должно быть способно рассуждать. Это делает их отличными.
В библиотеке dispatch есть 2 вида очередей и соответствующих им уровня API:
- глобальные очереди, которые не являются очередями подобными другим. И в действительности они — это только абстракция над пулом потоков.
- все остальные очереди, которые Вы можете устанавливать целевыми одна для другой как захотите.
На сегодняшний день стало ясно, что это была ошибка и что должно быть 3 вида очередей:
- глобальные очереди, которые не являются настоящими очередями, но представляют, то какое семейство системных атрибутов требует Ваш контекст исполнения (в основном приоритеты). И мы должны запретить отправку задач непосредственно в эти очереди.
- нижние очереди (которые GCD в последние годы отслеживает и называет «базами» в исходном коде (похоже, имеется в виду исходный код самой GCD — прим. перев.). Нижние очереди известны ядру, когда в них есть задачи.
- любые другие «внутренние» очереди, о которых ядро вообще не знает.
В группе разработки dispatch мы каждый проходящий день сожалеем, что различие между второй и третьей группами очередей не было изначально сделано ясным в API.
Мне нравится называть вторую группу «контекстами исполнения», но я могу понять почему Вы хотите назвать их Акторами. Это, возможно, более единообразно (и GCD поступила таким же образом, представив и то, и то как очереди). Такие верхнеуровневые «Акторы» должны быть немногочисленны потому, что если они все станут активными одновременно, то им будет нужно такое же количество потоков в процессе. И это не тот ресурс, который можно масштабировать. Вот почему важно различать их. И, как мы и обсуждаем, они также обычно используются для защиты разделяемого состояния, ресурса или чего-то подобного. Сделать это используя внутренние акторы, возможно, не получится.
<...>
- lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170828/039429.html
- lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170904/039461.html
Начните с последовательного исполнения
- twitter.com/pedantcoder/status/1081658384577835009
- twitter.com/pedantcoder/status/1081659784841969665
- twitter.com/pedantcoder/status/904839926180569089
- twitter.com/pedantcoder/status/904840156330344449
Не используйте глобальные очереди
- twitter.com/pedantcoder/status/773903697474486273
- lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170828/039368.html
Опасайтесь конкурентных очередей
Не используйте async-вызовы для защиты разделяемого состояния
- twitter.com/pedantcoder/status/820473404440489984
- twitter.com/pedantcoder/status/820473580819337219
- twitter.com/pedantcoder/status/820740434645221376
- twitter.com/pedantcoder/status/904467942208823296
- twitter.com/pedantcoder/status/904468363149099008
- twitter.com/pedantcoder/status/820473711606124544
- twitter.com/pedantcoder/status/820473923527589888
Не используйте async-вызовы для маленьких задач
- twitter.com/pedantcoder/status/1081657739451891713
- twitter.com/pedantcoder/status/1081642189048840192
- twitter.com/pedantcoder/status/1081642631732457472
- twitter.com/pedantcoder/status/1081648778975707136
Некоторые классы/библиотеки должны просто быть синхронными
Борьба параллельных задач между собой — это убийца производительности
- twitter.com/pedantcoder/status/1081657739451891713
- twitter.com/pedantcoder/status/1081658172610293760
Чтоб избежать взаимоблокировок, используйте механизмы блокировки, когда нужно защитить разделяемое состояние
Не используйте семафоры для ожидания асинхронной задачи
API NSOperation имеет несколько серьезных ловушек, которые могут привести к падению производительности
- twitter.com/pedantcoder/status/1082097847653154817
- twitter.com/pedantcoder/status/1082111968700289026
- lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170828/039415.html
Избегайте микро тестов производительности
- twitter.com/pedantcoder/status/1081660679054999552
- twitter.com/Catfish_Man/status/1081673457182490624
Ресурсы не безграничны
- twitter.com/pedantcoder/status/1081661310771712001
- lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170828/039410.html
- lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170828/039429.html
О dispatch_async_and_wait()
Использование 3-4 ядер — это не что-то простое
Множество улучшений производительности в iOS 12 были достигнуты благодаря однопоточным демонам
iWheelBuy
Очень странно видеть отрицательную оценку к посту, который является переводом и не видеть комментариев поясняющих суть минусов. Перевод адекватный, автору — большое спасибо.
artemvkepke Автор
Спасибо, приятно получить положительную оценку. Мне стоило добавить примечание в начало. Немного поправил.