Чем отличаются threading, multiprocessing и asyncio?
- Когда использовать потоки, а когда процессы?
- Что выбрать для параллельных HTTP-запросов?
- Как распараллелить вычисления в Python?
Потоки и asyncio — для ожидания ввода-вывода, процессы — для вычислений на нескольких ядрах.
- threading: несколько потоков одного процесса, общая память, ограничены GIL
- multiprocessing: отдельные процессы со своей памятью и своим GIL, данные передаются сериализацией
- asyncio: один поток, кооперативная многозадачность, тысячи соединений дёшево
А ещё Для типового выбора удобен concurrent.futures: ThreadPoolExecutor / ProcessPoolExecutor
Подробно
threading. Потоки внутри одного процесса делят память, поэтому обмен данными простой, но общее состояние надо защищать блокировками. Из-за GIL байткод выполняется одним потоком за раз, поэтому потоки подходят для I/O-bound задач: сетевые запросы, работа с диском, ожидание базы. Переключает потоки операционная система — в любой момент.
multiprocessing. Отдельные процессы с собственным интерпретатором — настоящий параллелизм на ядрах. Подходит для CPU-bound задач: расчёты, обработка изображений, парсинг больших файлов. Цена — запуск процессов дороже, память не общая, аргументы и результаты сериализуются (pickle).
asyncio. Один поток и цикл событий. Корутины сами отдают управление в точках await — пока одна ждёт ответа сети, работают другие. Тысячи одновременных соединений обходятся дёшево. Требует асинхронных библиотек (aiohttp, asyncpg); блокирующий вызов внутри корутины останавливает весь цикл.
Как выбирать:
- много сетевых запросов, веб-сервер — asyncio (или потоки, если библиотеки синхронные);
- немного параллельного ввода-вывода в синхронном коде — ThreadPoolExecutor;
- тяжёлые вычисления — ProcessPoolExecutor.
Пример
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import urllib.request
def fetch(url):
with urllib.request.urlopen(url, timeout=10) as r:
return len(r.read())
urls = ["https://example.com"] * 10
with ThreadPoolExecutor(max_workers=10) as pool: # I/O — потоки
sizes = list(pool.map(fetch, urls))
def heavy(n):
return sum(i * i for i in range(n))
if __name__ == "__main__":
with ProcessPoolExecutor() as pool: # CPU — процессы
results = list(pool.map(heavy, [10**7] * 4))Что хотят услышать
- Связь выбора с типом задачи: I/O-bound или CPU-bound.
- Роль GIL для потоков.
- Цена процессов: память, сериализация, запуск.
- Модель asyncio: один поток,
await, опасность блокирующих вызовов.
Частые ошибки
- Распараллеливать вычисления потоками и ждать ускорения.
- Вызывать
time.sleepили синхронныйrequestsвнутри корутины.
Проверьте себя
Один верный вариант. Подумайте, потом откройте ответ.
Показать варианты и ответ
- ✓Потоки и asyncio — для ожидания ввода-вывода, процессы — для вычислений на ядрах
- ✗Потоки — для тяжёлых вычислений на всех ядрах, asyncio — для ввода-вывода
- ✗asyncio запускает каждую задачу в отдельном процессе
- ✗multiprocessing ограничен одним GIL на все процессы
Что спросят дальше
- Как работают async и await? Junior
- Что такое GIL? Junior
Разбор темы
- Потоки, процессы и GIL
Что такое GIL и что он не защищает, когда выбирать потоки, процессы или asyncio, concurrent.futures, ограничения multiprocessing, сборка без GIL.
Связанные вопросы
Разбор собеседования с sotto
После собеседования sotto разбирает разговор: оценка, каждый вопрос — хорошо, частично или слабо, чего не хватило в ответе и над чем поработать к следующему разу. Первые 30 минут бесплатно.