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

Чем плохо использование потоков нежели кооперативной многозадачности в Python?

2.0 Middle🔥 191 комментариев
#Python Core#Асинхронность и многопоточность

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

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

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

Основная проблема: Global Interpreter Lock (GIL)

В Python потоки имеют фундаментальное ограничение — Global Interpreter Lock (GIL). Это означает, что только один поток может выполнять Python-код одновременно, даже на многоядерных процессорах. Это делает threading совершенно неэффективным для CPU-bound задач.

import threading
import time

def cpu_bound_task(n):
    total = 0
    for i in range(n):
        total += i ** 2
    return total

# Потоки НЕ распараллеливают CPU-bound
start = time.time()
t1 = threading.Thread(target=cpu_bound_task, args=(100_000_000,))
t2 = threading.Thread(target=cpu_bound_task, args=(100_000_000,))
t1.start()
t2.start()
t1.join()
t2.join()
print(f'Потоки: {time.time() - start:.2f}s')  # ~10 сек (медленнее, чем последовательно)

Преимущества кооперативной многозадачности (asyncio)

asyncio и другие async-фреймворки (aiohttp, FastAPI) используют event loop вместо OS-потоков. Это позволяет:

  1. Избежать GIL — async код работает в одном потоке
  2. Малые накладные расходы — корутины легче, чем потоки
  3. Предсказуемое переключение контекста — явное
  4. Масштабируемость — тысячи одновременных операций
import asyncio
import aiohttp
import time

async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()

async def fetch_many_urls(urls):
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, url) for url in urls]
        return await asyncio.gather(*tasks)

# Кооперативная многозадачность: 10 URL загружаются почти параллельно
urls = ['https://example.com'] * 10
start = time.time()
results = asyncio.run(fetch_many_urls(urls))
print(f'Asyncio: {time.time() - start:.2f}s')  # ~1 сек вместо 10

Проблемы потоков в Python

1. Отсутствие true parallelism

Потоки переключаются ОС случайно, но GIL блокирует выполнение Python-кода. Это 'кажущийся' параллелизм.

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

Без явной синхронизации (, , ) данные повреждаются:

import threading

shared_counter = 0

def increment():
    global shared_counter
    for _ in range(100_000):
        shared_counter += 1  # ОПАСНО! Race condition

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

print(shared_counter)  # Вместо 1_000_000 будет ~600_000

3. Deadlock и сложность отладки

Множество блокировок ведут к deadlock'ам:

import threading

lock1 = threading.Lock()
lock2 = threading.Lock()

def thread1_func():
    with lock1:
        print('Thread 1 locked lock1')
        time.sleep(0.1)
        with lock2:  # Ждёт lock2, которую держит thread2
            pass

def thread2_func():
    with lock2:
        print('Thread 2 locked lock2')
        with lock1:  # Deadlock!
            pass

# Потоки зависнут

4. Контекст-переключение дорого

ОС создаёт для каждого потока отдельный стек (1-8 МБ). Потоки переключаются часто — это вызывает cache miss и замедляет программу.

Когда всё-таки нужны потоки?

Только для I/O-bound операций, если asyncio невозможен:

  • Блокирующие библиотеки (requests, старые БД-драйверы)
  • Многопроцессные вычисления через multiprocessing
  • Интеграция с C-библиотеками без GIL
from concurrent.futures import ThreadPoolExecutor
import requests

def fetch_url(url):
    return requests.get(url).text  # Блокирующая операция

with ThreadPoolExecutor(max_workers=10) as executor:
    results = executor.map(fetch_url, urls)  # ОК для I/O

Вывод

  • Потоки — плохо для Python: GIL, сложная синхронизация, медленнее asyncio для I/O
  • asyncio — лучший выбор для I/O-bound (веб-серверы, API, БД)
  • multiprocessing — для CPU-bound вычислений (обходит GIL)

Используй потоки только если совсем нет альтернатив.