← Назад к вопросам
В чем разница между потоками и механизмом async/await в Python?
2.3 Middle🔥 211 комментариев
#Python Core#Асинхронность и многопоточность
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Потоки (Threads) vs async/await в Python: полное руководство
Это один из самых важных вопросов в Python. Оба механизма позволяют параллелизм, но работают принципиально по-разному. Путаница между ними — частая причина багов и проблем с производительностью.
GIL (Global Interpreter Lock) — сердце проблемы
Всё начинается с GIL. В Python есть глобальная блокировка, которая позволяет только одному потоку выполнять Python код одновременно. Это сделано для упрощения управления памятью.
# Важно понимать: GIL блокирует Python код, но НЕ IO операции
import threading
import time
def cpu_bound():
"""CPU-bound задача — потоки НЕ помогут"""
total = 0
for i in range(500000000):
total += i
return total
def io_bound():
"""IO-bound задача — потоки ПОМОГУТ"""
requests.get('https://api.example.com/data') # GIL отпускается
# CPU-bound: потоки выполняются поочередно, совсем не быстрее
# IO-bound: пока один поток ждёт IO, другой может выполнять код
Потоки (Threads)
Как работают:
- ОС управляет потоками
- Контекстное переключение: ОС прерывает поток A, запускает поток B
- Преимущество: истинный параллелизм для IO операций
- Недостаток: GIL не позволяет параллельный CPU код, нужна синхронизация
Преимущества потоков:
- Простой синтаксис: просто вызов функции в отдельном потоке
- CPU операции с multiprocessing работают истинно параллельно (обходит GIL)
- Работают везде, в том числе в sync коде
Недостатки потоков:
- GIL для CPU-bound задач
- Race conditions (нужны Lock, RLock, Semaphore)
- Deadlock опасность
- Контекстное переключение дорого (1000+ потоков = проблема)
- Дебаг сложный (racing conditions, timing зависимости)
import threading
import requests
import time
def fetch_url(url, results, index):
"""Каждый вызов работает в отдельном потоке"""
response = requests.get(url)
results[index] = response.status_code
time.sleep(1) # Имитация IO операции
urls = ['http://api1.com'] * 3
results = [None] * len(urls)
start = time.time()
threads = []
for i, url in enumerate(urls):
t = threading.Thread(target=fetch_url, args=(url, results, i))
threads.append(t)
t.start()
for t in threads:
t.join()
print(f"Потоки: {time.time() - start:.2f}s") # ~1s (параллельно)
async/await
Как работает:
- Одно-поточная программа (single thread)
- Корутины добровольно отдают управление (yield control)
- Event loop переключается между корутинами
- Преимущество: нет контекстного переключения, минимальные накладные расходы
- Недостаток: сложнее писать, не подходит для CPU-bound
import asyncio
import aiohttp
async def fetch_url(session, url):
"""Корутина отдаёт управление на await"""
async with session.get(url) as response:
# Здесь корутина ПРИОСТАНАВЛИВАЕТСЯ
# Event loop может запустить другую корутину
return response.status
async def main():
async with aiohttp.ClientSession() as session:
urls = ['http://api1.com'] * 3
tasks = [fetch_url(session, url) for url in urls]
# Все корутины выполняются параллельно
results = await asyncio.gather(*tasks)
return results
import time
start = time.time()
asyncio.run(main())
print(f"Async: {time.time() - start:.2f}s") # ~1s (параллельно, но одна очередь)
Сравнительная таблица
| Параметр | Потоки | async/await |
|---|---|---|
| Язык выполнения | Многопоточное | Однопоточное (event loop) |
| Управление | ОС (preemptive) | Сама программа (cooperative) |
| GIL | Отпускается на IO | Отпускается на await |
| CPU-bound | Медленно (GIL) | Очень медленно (nope) |
| IO-bound | Быстро | Очень быстро |
| Масштабируемость | До ~100 потоков | До 10,000+ корутин |
| Синтаксис | Простой | Требует async/await везде |
| Синхронизация | Lock, RLock, Semaphore | Не нужна (однопоточное) |
| Дебаг | Сложный (race conditions) | Проще |
| Deadlock | Возможен | Невозможен (однопоточное) |
| CPU Intensive | multiprocessing | ProcessPoolExecutor |
Практические примеры
IO-bound с потоками:
import threading
import requests
def download_file(url, filename):
data = requests.get(url).content
with open(filename, 'wb') as f:
f.write(data)
urls = [
('http://example.com/file1.zip', 'file1.zip'),
('http://example.com/file2.zip', 'file2.zip'),
('http://example.com/file3.zip', 'file3.zip'),
]
threads = []
for url, filename in urls:
t = threading.Thread(target=download_file, args=(url, filename))
threads.append(t)
t.start()
for t in threads:
t.join()
IO-bound с async:
import asyncio
import aiohttp
async def download_file(session, url, filename):
async with session.get(url) as resp:
data = await resp.read()
with open(filename, 'wb') as f:
f.write(data)
async def main():
async with aiohttp.ClientSession() as session:
tasks = [
download_file(session, 'http://example.com/file1.zip', 'file1.zip'),
download_file(session, 'http://example.com/file2.zip', 'file2.zip'),
download_file(session, 'http://example.com/file3.zip', 'file3.zip'),
]
await asyncio.gather(*tasks)
asyncio.run(main())
CPU-bound с multiprocessing (обходит GIL):
from multiprocessing import Pool
def cpu_heavy_task(n):
total = 0
for i in range(n):
total += i
return total
if __name__ == '__main__':
with Pool(4) as p: # 4 процесса, настоящий параллелизм
results = p.map(cpu_heavy_task, [50000000] * 4)
print(results)
Когда использовать что
Потоки (threading):
- ✓ IO-bound задачи (файлы, сеть, БД)
- ✓ Когда нужна совместимость (не везде есть async)
- ✓ Когда нужна простота синтаксиса
- ✗ CPU-bound (GIL помешает)
- ✗ Много одновременных задач (1000+)
async/await:
- ✓ IO-bound задачи, когда нужна масштабируемость
- ✓ Много одновременных соединений (веб-скрейпинг, веб-сокеты)
- ✓ Real-time приложения (чаты, игры)
- ✗ CPU-bound (async не поможет)
- ✗ Блокирующие операции (БД запросы без async драйвера)
multiprocessing:
- ✓ CPU-bound задачи
- ✓ Истинный параллелизм (обходит GIL)
- ✗ Overhead (процессы дороже потоков)
Типичная ошибка
# ❌ НЕПРАВИЛЬНО: async функция с синхронным модулем
import asyncio
import time
async def bad_async():
time.sleep(10) # БЛОКИРУЕТ весь event loop!
return "ready"
# Другие корутины не будут выполняться 10 секунд
# ✅ ПРАВИЛЬНО: используй async-совместимый модуль
async def good_async():
await asyncio.sleep(10) # Отдаёт управление event loop
return "ready"
Тестирование производительности
import asyncio, threading, time
import aiohttp, requests
def test_threads():
start = time.time()
threads = []
for i in range(10):
def fetch():
requests.get('http://httpbin.org/delay/1')
t = threading.Thread(target=fetch)
threads.append(t)
t.start()
for t in threads:
t.join()
return time.time() - start
async def test_async():
async def fetch(session):
async with session.get('http://httpbin.org/delay/1') as r:
await r.text()
start = time.time()
async with aiohttp.ClientSession() as session:
tasks = [fetch(session) for _ in range(10)]
await asyncio.gather(*tasks)
return time.time() - start
print(f"Потоки: {test_threads():.2f}s")
print(f"Async: {test_async():.2f}s")
# Оба должны быть ~1 секунда
Заключение
- Потоки — простое решение для IO-bound, но не масштабируется
- async/await — лучше для масштабирования, но требует async везде
- multiprocessing — единственный способ настоящего параллелизма для CPU
Для интервью: выбор между потоками и async зависит от масштаба: 10 одновременных задач — потоки, 1000+ — async.