Вопрос с собеседования · Python

Чем отличаются threading, multiprocessing и asyncio?

JuniorКонкурентность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 на все процессы

Что спросят дальше

Разбор темы

Связанные вопросы

Партнёр платформы

Разбор собеседования с sotto

После собеседования sotto разбирает разговор: оценка, каждый вопрос — хорошо, частично или слабо, чего не хватило в ответе и над чем поработать к следующему разу. Первые 30 минут бесплатно.

Разбор собеседования