← Назад к вопросам

Почему в многопоточности больше накладных расходов, чем в асинхронности?

1.0 Junior🔥 201 комментариев
#Асинхронность и многопоточность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Почему в многопоточности больше накладных расходов, чем в асинхронности

Это глубокий вопрос о том, как OS и CPU распределяют ресурсы. Давайте разберёмся на практических примерах.

1. Context Switching — главный враг многопоточности

Многопоточность (Threading):

  • OS создаёт отдельный thread для каждой задачи
  • OS сам управляет переключением между потоками (preemptive scheduling)
  • При переключении OS сохраняет весь контекст потока (регистры CPU, стек, кэш)
# Многопоточность: OS переключается между потоками силой
import threading
import time

def worker():
    for i in range(1000000):
        x = i * 2  # Работа

# 4 потока = OS постоянно переключается между ними
threads = [threading.Thread(target=worker) for _ in range(4)]
for t in threads:
    t.start()
for t in threads:
    t.join()

Асинхронность (Async/Await):

  • Один thread, но много coroutine (лёгкие объекты в памяти)
  • Сам программист решает когда передать управление (await)
  • Переключение — это просто скачок на следующую coroutine в памяти
# Асинхронность: программист контролирует переключение
import asyncio

async def worker():
    for i in range(1000000):
        if i % 100000 == 0:
            await asyncio.sleep(0)  # Даю управление другим
        x = i * 2

# 4 coroutine в одном потоке
await asyncio.gather(*[worker() for _ in range(4)])

Стоимость context switch (примерно):

  • L1 кэш процессора теряется (нужно перезаполнять)
  • L2 кэш может теряться
  • TLB (translation lookaside buffer) очищается
  • Результат: теряются сотни/тысячи циклов CPU на переключение

2. Память — потоки дорогие

Многопоточность:

  • Каждый thread занимает 8 MB памяти (на Linux 64-bit)
  • Стек потока, TCB (thread control block), синхронизационные структуры
  • Если создашь 1000 потоков — это уже 8 GB памяти!
import threading
import sys

# Сколько памяти занимает пустой thread?
print(f"Размер thread object: {sys.getsizeof(threading.Thread())} байт")
# Но реальный стек: ~8 MB на OS уровне

# Практическая демонстрация
import resource
print(resource.getrlimit(resource.RLIMIT_STACK))  # (8388608, ...) — 8 MB

Асинхронность:

  • Coroutine занимает ~100 байт памяти
  • Можно создать 100 000 coroutine в том же объёме памяти что 10 потоков
import asyncio
import sys

async def dummy():
    await asyncio.sleep(0)

# Создаём 10 000 coroutine
tasks = [asyncio.create_task(dummy()) for _ in range(10000)]
# Всё это займёт несколько МБ, не 80 GB

3. Lock Contention и Mutex

Многопоточность требует синхронизации:

  • Когда несколько потоков обращаются к общему ресурсу, нужны locks/mutexes
  • Поток теряет очередь на lock (busy-waiting или context switch)
  • Может возникнуть deadlock или starvation
import threading

# Проблема: все потоки ждут одного lock
shared_dict = {}
lock = threading.Lock()

def increment():
    for _ in range(100000):
        with lock:  # Все потоки ждут здесь!
            shared_dict['count'] = shared_dict.get('count', 0) + 1

threads = [threading.Thread(target=increment) for _ in range(4)]
# Много context switch при борьбе за lock

Асинхронность — нет shared state по умолчанию:

import asyncio

# GIL + однопоточность = нет race conditions автоматически
counter = 0

async def increment():
    global counter
    for _ in range(100000):
        counter += 1  # Безопасно без lock! (но медленнее)

await asyncio.gather(*[increment() for _ in range(4)])

4. GIL (Global Interpreter Lock) в Python

Многопоточность в Python:

  • Python GIL позволяет только одному потоку выполнять код одновременно
  • Остальные потоки ждут
  • Context switch между потоками = чистая потеря производительности (не реальный параллелизм)
import threading
import time

def cpu_bound():
    for _ in range(50000000):
        x = 1 + 1

start = time.time()

# 1 поток
t = threading.Thread(target=cpu_bound)
t.start()
t.join()
print(f"1 thread: {time.time() - start:.2f}s")

start = time.time()
# 4 потока медленнее из-за GIL!
threads = [threading.Thread(target=cpu_bound) for _ in range(4)]
for t in threads:
    t.start()
for t in threads:
    t.join()
print(f"4 threads: {time.time() - start:.2f}s")  # Медленнее!

Асинхронность избегает GIL:

  • Только один event loop + coroutine в памяти
  • GIL не рассчитан на context switching между coroutines
  • Нет overhead GIL contention

5. Практический пример: I/O операции

import threading
import asyncio
import time
import httpx

# МНОГОПОТОЧНОСТЬ
def fetch_threaded(url):
    requests.get(url)

start = time.time()
threads = [
    threading.Thread(target=fetch_threaded, args=(url,))
    for url in urls
]
for t in threads:
    t.start()
for t in threads:
    t.join()
print(f"Threading: {time.time() - start:.2f}s")

# АСИНХРОННОСТЬ
async def fetch_async(url):
    async with httpx.AsyncClient() as client:
        await client.get(url)

async def main():
    tasks = [fetch_async(url) for url in urls]
    await asyncio.gather(*tasks)

start = time.time()
asyncio.run(main())
print(f"Async: {time.time() - start:.2f}s")  # Быстрее!

Почему async быстрее?

  • Нет context switch overhead (1 поток)
  • Нет mutex contention
  • Нет GIL overhead
  • Одна coroutine ждёт I/O, другие продолжают работать

Итог: Когда что использовать?

СценарийВыборПричина
I/O-bound (сеть, БД)AsyncНет overhead, масштабируется до 100K concurrent
CPU-bound (вычисления)multiprocessing (не threading)GIL + многопоточность бесполезна
Простой скриптThreading OKДля 2-10 потоков overhead незначительный
Высоконагруженный серверAsync + UvicornТысячи одновременных подключений

Аналогия: Многопоточность — это как нанять 4 работников и постоянно их отвлекать (context switch). Асинхронность — это как один умный рабочий, который знает когда пауза и может переключиться на другую задачу сам.