Конкурентность
Координация
Как координировать работу между потоками
🗓️ Координация касается того, как потоки общаются и передают работу друг другу. Один поток производит задачи, другой их потребляет. Один сервис отправляет запрос, другой сервис его обрабатывает. Как независимые пути выполнения сигнализируют друг другу без неэффективного использования CPU или повреждения состояния?
Проблема
Представьте, что вы создаете планировщик задач для веб-приложения. Некоторая работа просто не должна выполняться во время обработки запроса. Она занимает слишком много времени, и если запускать ее синхронно, каждый API-запрос будет блокироваться.
Поэтому мы выносим такую работу для фоновой обработки. Пользователи регистрируются, и им нужно отправить приветственные письма. Они загружают фотографии профиля, которые нужно изменить в размере. Администраторы запрашивают ежемесячные отчеты, генерация которых занимает минуты. Обработчики API ставят задачи в очередь, а пул рабочих потоков обрабатывает их асинхронно.
Концептуально архитектура проста: обработчики API производят задачи, рабочие потоки их потребляют, а между ними находится что-то, что координирует передачу.
Когда нагрузка стабильна, это работает хорошо, но в граничных случаях, таких как пиковые нагрузки, начинают появляться проблемы.
Во-первых, рассмотрим, что происходит, когда рабочие потоки готовы к работе, но работы нет. Самый наивный подход - просто продолжать проверять наличие работы, пока что-нибудь не появится.
while True:
if queue:
task = queue.pop(0)
execute(task)
Это busy-waiting, и он может быть катастрофическим. Каждый рабочий поток крутится в цикле, потребляя ресурсы CPU и не делая никакой полезной работы. С восемью рабочими потоками на восьмиядерной машине можно потреблять 100% вычислительной мощности, просто проверяя пустую очередь. Когда задачи наконец поступают, то не остается CPU для их выполнения.
Можно попытаться исправить это, засыпая, когда нет работы.
import time
while True:
if queue:
task = queue.pop(0)
execute(task)
else:
time.sleep(0.1)
Это снижает использование CPU, но теперь мы обменяли потери ресурсов на задержку. Задача, которая поступает через 1 мс после того, как рабочий поток засыпает, ждет почти 100 мс перед обработкой. Если спать дольше, задержка выполнения увеличивается. Если спать меньше, мы снова будем впустую сжигать CPU.
Теперь перевернем проблему. Что происходит, когда производители работают быстрее потребителей?
Допустим, отправляется маркетинговое письмо, и 50 000 пользователей одновременно нажимают на ссылку. Каждый запрос ставит в очередь фоновую работу.
09:00:00.000 - Queue size: 0
09:00:00.100 - Queue size: 5,000
09:00:00.200 - Queue size: 12,000
09:00:00.300 - Queue size: 23,000
09:00:00.400 - Queue size: 38,000
09:00:00.500 - Queue size: 50,000Ваши восемь рабочих потоков могут обрабатывать, допустим, 100 задач в секунду,
что означает, что очередь из тысяч задач будет разгружаться минуты. Однако сама
по себе задержка - это не ключевая проблема. Настоящая проблема - это память.
Каждая отдельная задача в этой очереди является объектом в куче, потребляющим
память. Если очередь не ограничена и продолжает принимать новые задачи, она
просто растет, пока в конечном итоге мы не столкнемся с OutOfMemoryError.
Когда это происходит, падает весь сервис. Не только фоновая обработка, но и API
перестает работать.
Это проблема координации. Как потоки общаются и упорядочивают свою работу? Им нужно сигнализировать друг другу "работа готова", эффективно ждать без использования CPU и справляться с ситуацией, когда одна сторона быстрее другой. Нужно решить три задачи:
- Эффективное ожидание - потребители должны спать, когда работы нет, и просыпаться сразу, когда она появляется.
- Backpressure - производители должны замедляться, когда потребители не успевают, чтобы не допустить исчерпания памяти.
- Потокобезопасность - сам механизм координации должен справляться с конкурентным доступом без повреждения состояния.
Решения
Существует два принципиально разных подхода к решению этих проблем. Координация общего состояния использует структуры данных, к которым несколько потоков обращаются напрямую, например очередь, в которую производители кладут данные, а потребители забирают. Координация передачи сообщений полностью избегает общего состояния. У каждого компонента есть свой собственный "почтовый ящик", и он общается путем отправки сообщений.
Давайте рассмотрим оба варианта.
Перейдите на Premium, чтобы продолжить
Разблокируйте доступ к этой статье и всем остальным материалам с NowInterview Premium
Перейти на Premium