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

В чем разница между потоками и механизмом 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)

Как работают:

  1. ОС управляет потоками
  2. Контекстное переключение: ОС прерывает поток A, запускает поток B
  3. Преимущество: истинный параллелизм для IO операций
  4. Недостаток: 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

Как работает:

  1. Одно-поточная программа (single thread)
  2. Корутины добровольно отдают управление (yield control)
  3. Event loop переключается между корутинами
  4. Преимущество: нет контекстного переключения, минимальные накладные расходы
  5. Недостаток: сложнее писать, не подходит для 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 IntensivemultiprocessingProcessPoolExecutor

Практические примеры

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.

В чем разница между потоками и механизмом async/await в Python? | PrepBro