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

Чем асинхронность лучше многопоточности в Python?

3.0 Senior🔥 171 комментариев
#Python Core#Асинхронность и многопоточность

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

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

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

Асинхронность vs Многопоточность в Python

Оба подхода используются для конкурентного программирования, но имеют принципиально разные преимущества и недостатки в контексте Python.

Основные различия

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

  • Несколько потоков работают параллельно
  • Переключение контекста управляется операционной системой
  • Используется GIL (Global Interpreter Lock) в CPython
  • Может быть непредсказуемой из-за race conditions

Асинхронность (async/await):

  • Один поток, но конкурентное выполнение
  • Переключение контекста управляется программистом (event loop)
  • Нет GIL
  • Более предсказуемо

1. Проблема с GIL (Global Interpreter Lock)

Многопоточность в Python имеет серьёзное ограничение:

import threading
import time

def cpu_intensive_task(n):
    """CPU-bound операция (много вычислений)"""
    total = 0
    for i in range(n):
        total += i
    return total

# Однопоточное выполнение
start = time.time()
cpu_intensive_task(100000000)
cpu_intensive_task(100000000)
print(f"Однопоток: {time.time() - start:.2f}s")  # ~2.5 сек

# Двухпоточное выполнение
start = time.time()
t1 = threading.Thread(target=cpu_intensive_task, args=(100000000,))
t2 = threading.Thread(target=cpu_intensive_task, args=(100000000,))
t1.start()
t2.start()
t1.join()
t2.join()
print(f"Два потока: {time.time() - start:.2f}s")  # ~2.8 сек (медленнее!)

# GIL не позволяет одновременное выполнение CPU операций
# Потоки конкурируют за GIL, замедляя работу

С asyncio (для I/O операций):

import asyncio
import time

async def io_operation(name, delay):
    """I/O операция (сетевой запрос, чтение файла)"""
    print(f"Начало: {name}")
    await asyncio.sleep(delay)  # Имитация I/O
    print(f"Конец: {name}")
    return f"Результат {name}"

async def main():
    start = time.time()
    
    # Запустить 3 операции параллельно в одном потоке
    results = await asyncio.gather(
        io_operation("Task 1", 2),
        io_operation("Task 2", 2),
        io_operation("Task 3", 2),
    )
    
    elapsed = time.time() - start
    print(f"Время: {elapsed:.2f}s")  # ~2 сек (не 6!)
    return results

asyncio.run(main())
# Asyncio не ограничена GIL для I/O операций

2. Race Conditions и синхронизация

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

import threading

class Counter:
    def __init__(self):
        self.value = 0
        self.lock = threading.Lock()  # Нужен для безопасности
    
    def increment(self):
        with self.lock:  # Критическая секция
            self.value += 1

counter = Counter()

# Без lock'а может быть race condition
def unsafe_increment():
    # Трёхшаговая операция (не атомарная)
    temp = counter.value
    temp += 1
    counter.value = temp

threads = []
for _ in range(10):
    t = threading.Thread(target=unsafe_increment)
    threads.append(t)
    t.start()

for t in threads:
    t.join()

print(counter.value)  # Может быть меньше 10 из-за race condition!

С asyncio нет race conditions при правильном использовании:

import asyncio

class AsyncCounter:
    def __init__(self):
        self.value = 0
        # Не нужен lock, так как всё в одном потоке
    
    async def increment(self):
        self.value += 1
        await asyncio.sleep(0)  # Позволить другим корутинам работать

async def main():
    counter = AsyncCounter()
    
    async def worker():
        for _ in range(10):
            await counter.increment()
    
    # Запустить 10 workers
    await asyncio.gather(*[worker() for _ in range(10)])
    
    print(counter.value)  # Ровно 100, race condition невозможна

asyncio.run(main())

3. Потребление памяти

Каждый поток потребляет память:

import threading
import sys

# Примерный размер потока
print(f"Размер потока: ~{sys.getsizeof(threading.Thread())} байт")

# С 1000 потоков: ~1000 * 2МБ (stack) = 2ГБ памяти
# С 10000 потоков: бутерброд из памяти

Asyncio намного экономнее:

import asyncio

# Корутина занимает ~1KB (вместо 2МБ для потока)
# С 10000 корутин: ~10МБ (vs 20ГБ для потоков!)

4. Масштабируемость

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

  • Обычно max 100-1000 потоков
  • Context switching дорогой
  • Синхронизация сложная

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

  • Можешь создать 100,000+ корутин
  • Дешёвое переключение контекста
  • Простая синхронизация
import asyncio
import time

async def worker(n):
    await asyncio.sleep(0.1)
    return n

async def main():
    # Создать 10,000 конкурентных задач
    start = time.time()
    results = await asyncio.gather(
        *[worker(i) for i in range(10000)]
    )
    elapsed = time.time() - start
    print(f"10,000 корутин за {elapsed:.2f}s")

asyncio.run(main())

5. Отладка и предсказуемость

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

import threading
import time
import random

def flaky_function():
    """Может работать по-разному из-за timing issues"""
    local_var = 0
    for _ in range(100):
        local_var += 1
    # Может быть race condition в других местах
    return local_var

# Бага может появляться случайно, быть невоспроизводимой

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

import asyncio

async def predictable_function():
    # Очень предсказуемо, нет неожиданных переключений
    local_var = 0
    for _ in range(100):
        local_var += 1
        await asyncio.sleep(0)  # Явное переключение
    return local_var

6. Сравнение для разных типов задач

I/O-bound (сетевые запросы, БД):

# Асинхронность ЛУЧШЕ
# - Нет блокировки
# - Экономия памяти
# - Проще синхронизация

import asyncio
import aiohttp

async def fetch_many():
    async with aiohttp.ClientSession() as session:
        tasks = [
            session.get(f"https://api.example.com/item/{i}")
            for i in range(1000)
        ]
        results = await asyncio.gather(*tasks)

CPU-bound (вычисления):

# Многопроцессность ЛУЧШЕ (не threading!)
# Асинхронность НЕ помогает

from multiprocessing import Pool

def calculate(n):
    return sum(i**2 for i in range(n))

if __name__ == '__main__':
    with Pool(4) as pool:
        results = pool.map(calculate, [100000]*1000)

Смешанные задачи:

import asyncio
from concurrent.futures import ProcessPoolExecutor

async def hybrid_work():
    # I/O операция асинхронно
    await asyncio.sleep(1)
    
    # CPU операция в отдельном процессе
    loop = asyncio.get_event_loop()
    result = await loop.run_in_executor(
        ProcessPoolExecutor(),
        cpu_intensive_function,
        100000
    )

7. Таблица сравнения

АспектThreadingAsynciomultiprocessing
GILОграниченаНе нуженПолностью свободна
Memory/корутина~2МБ~1KB~50МБ
Max конкурентных100-1000100,000+4-64
СинхронизацияСложная (Lock)ПростаяСредняя
I/O операцииOKОтличноПлохо
CPU операцииПлохоПлохоОтлично
ОтладкаСложноПрощеСредне
ИзучениеСреднееЛегчеСложнее

Заключение

Используй asyncio если:

  • I/O-bound операции (HTTP, БД, файлы)
  • Нужна высокая конкурентность
  • Хочешь простой синхронный код
  • Нужна предсказуемость

Используй threading если:

  • Немного потоков (< 50)
  • Смешанный код, который сложно переделать
  • Простые операции

Используй multiprocessing если:

  • CPU-bound операции
  • Нужна полная параллелизм на многоядерных системах

Асинхронность лучше для большинства Python приложений, особенно веб-сервисов и микросервисов!