← Назад к вопросам
Почему не стоит использовать многопоточность в 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))
Итоги
- GIL блокирует параллельное выполнение Python кода
- Многопоточность имеет overhead — создание и переключение потоков
- Асинхронность намного эффективнее для I/O — один поток, много корутин
- Для I/O используй asyncio, не threading
- Для CPU используй multiprocessing (или C extensions)
- Многопоточность имеет смысл только если:
- I/O операции отпускают GIL (C extensions)
- Нужна совместимость со старым синхронным кодом
Праило: I/O Bound + Python = asyncio, не threading!