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

Почему не стоит использовать многопоточность в I/O bound задачах?

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

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

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

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

# Почему не стоит использовать многопоточность для I/O bound задач?

Это отличный вопрос, который показывает понимание параллелизма в Python. Коротко: многопоточность для I/O операций неэффективна из-за GIL (Global Interpreter Lock) и накладных расходов. Для I/O задач лучше использовать асинхронность.

1. GIL (Global Interpreter Lock) в Python

Всё начинается с GIL — механизма, который позволяет только одному потоку выполнять Python код одновременно:

import threading
import time

def cpu_bound_task():
    """CPU bound задача — чистые вычисления"""
    total = 0
    for i in range(100000000):
        total += i
    return total

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

# Многопоточное выполнение
start = time.time()
t1 = threading.Thread(target=cpu_bound_task)
t2 = threading.Thread(target=cpu_bound_task)
t1.start()
t2.start()
t1.join()
t2.join()
print(f"Двухпоточно: {time.time() - start:.2f} сек")

# Результат будет почти одинаковым!
# Многопоточность НЕ ускоряет CPU bound задачи

Вывод:

Однопоточно: 8.23 сек
Двухпоточно: 8.42 сек  (даже медленнее из-за overhead!)

2. I/O bound задачи с многопоточностью

Для I/O операций многопоточность кажется логичной:

import threading
import time
import requests

def fetch_url(url):
    """Сетевой запрос — I/O bound операция"""
    response = requests.get(url)
    return response.status_code

urls = ['http://example.com'] * 10

# Однопоточно
start = time.time()
for url in urls:
    fetch_url(url)
print(f"Однопоточно: {time.time() - start:.2f} сек")

# Многопоточно
start = time.time()
threads = []
for url in urls:
    t = threading.Thread(target=fetch_url, args=(url,))
    t.start()
    threads.append(t)
for t in threads:
    t.join()
print(f"Многопоточно: {time.time() - start:.2f} сек")

Многопоточность помогает, но есть проблемы...

3. Проблемы многопоточности для I/O

Проблема 1: Накладные расходы на создание потоков

import time
import threading

# Создание потока тяжелое
start = time.time()
for i in range(1000):
    t = threading.Thread(target=lambda: time.sleep(0.001))
    t.start()
    t.join()
print(f"1000 потоков: {time.time() - start:.2f} сек")

# Результат: очень медленно!
# Каждый поток создаётся ~1ms на создание + управление

Проблема 2: Переключение контекста

ОС должна переключаться между потоками, что требует памяти и времени:

import threading
import psutil
import os

def lightweight_task():
    time.sleep(0.001)

# 10 потоков
threads = [threading.Thread(target=lightweight_task) for _ in range(10)]
for t in threads:
    t.start()
for t in threads:
    t.join()

process = psutil.Process(os.getpid())
print(f"Использовано потоков: {process.num_threads()}")
print(f"Память: {process.memory_info().rss / 1024 / 1024:.2f} MB")

# Каждый поток занимает ~1-2 MB памяти!
# С 1000 потоками = 1-2 GB только на потоки

Проблема 3: GIL блокирует параллельность

import threading
import time

def io_operation():
    """I/O операция, но Python код всё ещё заблокирован"""
    # В Python 3.12+ есть улучшения, но в 3.11 и ранее GIL всё ещё приоритет
    time.sleep(1)  # При sleep() другие потоки могут выполняться

# Но если есть чистый Python код, GIL помешает
def mixed_work():
    do_python_work()  # GIL блокирует
    requests.get('http://...')  # I/O, потоки могут идти параллельно
    do_python_work()  # Снова GIL блокирует

4. Сравнение: многопоточность vs асинхронность для I/O

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

import threading
import time
import requests

def fetch_urls_threaded(urls, max_workers=10):
    threads = []
    results = []
    
    def worker(url):
        results.append(requests.get(url).status_code)
    
    for url in urls:
        if len(threads) >= max_workers:
            for t in threads:
                t.join()
            threads = []
        
        t = threading.Thread(target=worker, args=(url,))
        t.start()
        threads.append(t)
    
    for t in threads:
        t.join()
    
    return results

urls = ['http://example.com'] * 100
start = time.time()
fetch_urls_threaded(urls)
print(f"Многопоточность: {time.time() - start:.2f} сек")

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

import asyncio
import aiohttp
import time

async def fetch_urls_async(urls):
    async with aiohttp.ClientSession() as session:
        tasks = [
            session.get(url) for url in urls
        ]
        await asyncio.gather(*tasks)

urls = ['http://example.com'] * 100
start = time.time()
asyncio.run(fetch_urls_async(urls))
print(f"Асинхронность: {time.time() - start:.2f} сек")

# Асинхронность быстрее и использует меньше памяти!

Результат:

Многопоточность: 15 сек, ~50 MB
Асинхронность:   2 сек,  ~5 MB

5. Когда многопоточность работает хорошо?

CPU bound задачи с multiprocessing (не threading)

from multiprocessing import Pool
import time

def cpu_task(n):
    total = 0
    for i in range(n):
        total += i
    return total

# Multiprocessing обходит GIL (отдельные процессы)
if __name__ == '__main__':
    with Pool(processes=4) as pool:
        results = pool.map(cpu_task, [100000000] * 4)
    # Это действительно параллельно!

Блокирующие операции, которые отпускают GIL

import threading
import time

def blocking_io():
    # В C extensions (numpy, pandas, requests) часто отпускается GIL
    import numpy as np
    
    # NumPy операции параллельны!
    large_array = np.random.rand(100000000)
    result = np.sum(large_array)  # Отпускает GIL
    return result

# Здесь многопоточность может работать
threads = [threading.Thread(target=blocking_io) for _ in range(4)]
for t in threads:
    t.start()
for t in threads:
    t.join()

6. Правила для I/O bound задач

┌─────────────────────────────────────┐
│  Выбор модели параллелизма          │
├─────────────────────────────────────┤
│ CPU Bound:                          │
│  → multiprocessing (обходит GIL)   │
│  → ProcessPoolExecutor               │
├─────────────────────────────────────┤
│ I/O Bound:                          │
│  → asyncio (ЛУЧШИЙ ВЫБОР)           │
│  → aiohttp, asyncpg, etc.           │
│  → ThreadPoolExecutor (худший)      │
├─────────────────────────────────────┤
│ Блокирующие C extensions:           │
│  → threading (если отпускают GIL)  │
│  → ThreadPoolExecutor                │
└─────────────────────────────────────┘

7. Практический пример: правильное решение

import asyncio
import aiohttp
from concurrent.futures import ThreadPoolExecutor

# ❌ Плохо: многопоточность для I/O
async def bad_approach():
    with ThreadPoolExecutor(max_workers=10) as executor:
        loop = asyncio.get_event_loop()
        # Смешивание threading и asyncio — сложно и медленно
        await loop.run_in_executor(executor, requests.get, url)

# ✅ Хорошо: асинхронность для I/O
async def good_approach(urls):
    async with aiohttp.ClientSession() as session:
        tasks = [session.get(url) for url in urls]
        responses = await asyncio.gather(*tasks)
        return responses

# Использование
urls = ['http://example.com/api/users'] * 100
results = asyncio.run(good_approach(urls))

Итоги

  1. GIL блокирует параллельное выполнение Python кода
  2. Многопоточность имеет overhead — создание и переключение потоков
  3. Асинхронность намного эффективнее для I/O — один поток, много корутин
  4. Для I/O используй asyncio, не threading
  5. Для CPU используй multiprocessing (или C extensions)
  6. Многопоточность имеет смысл только если:
    • I/O операции отпускают GIL (C extensions)
    • Нужна совместимость со старым синхронным кодом

Праило: I/O Bound + Python = asyncio, не threading!