← Назад к вопросам
Почему в многопоточности больше накладных расходов, чем в асинхронности?
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). Асинхронность — это как один умный рабочий, который знает когда пауза и может переключиться на другую задачу сам.