Прямой ответ
Модель конкурентности выбирается по характеру задачи: ожидание ввода-вывода выполняется в потоках либо в цикле событий, вычисления — в процессах.
| Характер задачи | Модель |
|---|---|
| Ожидание сети либо диска | Потоки либо asyncio |
| Вычисления на байткоде | Процессы |
| Вызов внешней библиотеки, освобождающей блокировку | Потоки |
| Малое число одновременных задач | Потоки |
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
with ThreadPoolExecutor(max_workers=16) as pool: # ожидание
...
with ProcessPoolExecutor(max_workers=4) as pool: # вычисления
...
Единый интерфейс пулов позволяет сменить модель без изменения кода отправки задач. О пулах задач — в статье про concurrent.futures.
Накладные расходы и передача данных
Стоимость создания и обмена определяет, даёт ли модель ускорение при малом объёме работы.
Создание потока. Поток создаётся быстрее процесса, поскольку адресное пространство общее. Накладные расходы заметны при малом объёме задачи.
Создание процесса. Процесс имеет собственное адресное пространство, поэтому создание занимает больше времени. Пул переиспользует процессы, поэтому накладные расходы распределяются.
Передача данных. Аргументы и результаты передаются между процессами через сериализацию, поэтому объём обмена влияет на время. Потоки используют общую память, поэтому передача не требуется.
Практическое следствие: многопроцессность даёт ускорение при объёме вычислений, превышающем накладные расходы на создание процесса и передачу данных.
Отдельный аспект — блокировка интерпретатора. Вычислительная задача удерживает блокировку, поэтому потоки не дают параллелизма. О влиянии — в статье про GIL.
Три признака неверного выбора модели.
Вычислительная задача в потоках. Блокировка удерживается, поэтому ускорения нет, а накладные расходы переключения добавляются.
Задача с ожиданием в процессах. Накладные расходы на сериализацию превышают выигрыш, поскольку блокировка освобождается при ожидании.
Малый объём работы на задачу. Накладные расходы создания превышают время выполнения, поэтому последовательное выполнение быстрее.
О выборе между asyncio и потоками — в статье про asyncio против потоков.
Измерение доли ожидания и вычислений
Выбор модели основывается на распределении времени между ожиданием и вычислениями, поэтому распределение измеряется.
Доля ожидания. Время, приходящееся на ожидание ответа, определяется профилированием. Высокая доля ожидания указывает на пригодность потоков либо цикла событий.
Доля вычислений. Время, приходящееся на обработку данных, указывает на пригодность процессов, поскольку блокировка интерпретатора удерживается.
Смешанная задача. Задача с сопоставимыми долями требует сочетания моделей, поэтому часть работы выполняется в потоках, часть — в процессах.
Практическое следствие: измерение распределения времени выполняется до выбора модели, поскольку предположение о характере задачи может не соответствовать фактическому. О профилировании — в статье про профилирование приложений.
Ограничение ресурсов и число работников
Число работников определяется доступными ресурсами, поэтому избыточное число не даёт дополнительного ускорения.
Вычислительные задачи. Число процессов соответствует числу доступных ядер, поскольку вычисления выполняются на ядрах. Избыточное число процессов увеличивает переключение без выигрыша.
Задачи с ожиданием. Число потоков превышает число ядер, поскольку поток в ожидании не занимает ядро. Предел определяется объёмом памяти на стеки и числом соединений.
Ограничение внешних ресурсов. Число одновременных обращений к внешнему сервису ограничивается, поэтому избыточное число работников приводит к отказам.
Практическое следствие: число работников выбирается по ограничивающему ресурсу — ядрам, памяти либо доступности внешнего сервиса. О защите внешнего сервиса — в статье про ограничение частоты запросов.
Практика: выбор модели конкурентности
1. Определите характер задачи. Ожидание ввода-вывода либо вычисления определяют пригодность модели.
2. Оцените объём работы на задачу. Малый объём делает накладные расходы создания заметными, поэтому последовательное выполнение быстрее.
3. Проверьте сериализуемость данных. Объект с ссылкой на несохраняемый ресурс не передаётся в процесс.
4. Измеряйте фактическое ускорение. Накладные расходы могут превышать выигрыш, поэтому измерение подтверждает выбор.
5. Используйте пул вместо создания на каждую задачу. Пул переиспользует работников, поэтому накладные расходы распределяются.
6. Выбирайте число работников по характеру задач. Для ожидания число превышает число ядер, для вычислений — соответствует числу ядер.
7. Сочетайте модели при необходимости. Блокирующие вызовы в асинхронном коде выносятся в пул потоков.
Рекомендации:
- Различайте конкурентность и параллелизм. Потоки и asyncio обеспечивают конкурентность при ожидании, процессы — параллелизм вычислений.
- Учитывайте стоимость сериализации. Объём передаваемых данных влияет на время, поэтому данные делятся на части.
- Проверяйте поведение внешней библиотеки. Библиотека, освобождающая блокировку, позволяет достичь параллелизма в потоках.
- Фиксируйте выбор модели в коде. Смена модели через единый интерфейс пулов не требует изменения логики задач.
- Пересматривайте при изменении характера задачи. Переход от ожидания к вычислениям требует смены модели.
Практический сценарий: смена модели не дала ускорения
Контекст: задача была переведена с потоков на процессы, но время выполнения не сократилось.
- Диагностика. Задача состояла из ожидания ответов внешних сервисов, поэтому блокировка интерпретатора освобождалась при ожидании. Потоки уже обеспечивали конкурентность, а переход на процессы добавил накладные расходы на сериализацию аргументов.
- Причина. Характер задачи был определён неверно: ожидание ввода-вывода не требует параллелизма вычислений. Многопроцессность добавила стоимость передачи данных без выигрыша.
- Действия. (а) Определили долю времени, приходящуюся на ожидание, и убедились, что задача относится к ожидающим; (б) вернулись к пулу потоков, увеличив число работников; (в) сократили объём передаваемых данных, чтобы накладные расходы не влияли; (г) измерили время выполнения при разном числе работников; (д) зафиксировали правило выбора модели по доле ожидания в общем времени.
- Результат. Время выполнения сократилось при увеличении числа потоков, поскольку задача относится к ожидающим.
Вывод: модель конкурентности выбирается по характеру задачи, а не по предположению. Задача с ожиданием ускоряется увеличением числа потоков, а переход на процессы добавляет накладные расходы без выигрыша.
FAQ
Какая модель подходит для вычислительной задачи? Процессы, поскольку каждый процесс имеет собственный интерпретатор и не конкурирует за блокировку. Потоки не дают параллелизма для вычислений.
Когда потоки не дают ускорения? При вычислительной задаче, поскольку блокировка интерпретатора удерживается. При малом объёме работы накладные расходы переключения превышают выигрыш.
Влияет ли объём передаваемых данных на выбор? Да, передача между процессами требует сериализации, поэтому большой объём обмена делает многопроцессность медленнее.
Можно ли сочетать модели? Да, асинхронный цикл событий выполняет основную работу, а блокирующие вызовы выносятся в пул потоков.
Источники: Python concurrent.futures, threading, multiprocessing — ссылки в шапке.