Насколько подходит многопоточность (multithreading) в Python для параллельных задач
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Многопоточность в Python для параллельных задач
Вопрос о многопоточности в Python — один из самых обсуждаемых в сообществе. Ответ зависит от типа задач, которые вы решаете.
Ограничение: Global Interpreter Lock (GIL)
В CPython (стандартной реализации) существует так называемый Global Interpreter Lock — механизм, который позволяет только одному потоку выполнять Python bytecode одновременно. Это означает, что многопоточность не даёт паралелизма для CPU-bound задач:
import threading
import time
def cpu_bound_task():
total = 0
for i in range(100_000_000):
total += i
return total
# Однопоточный вариант
start = time.time()
cpu_bound_task()
print(f"Single-threaded: {time.time() - start:.2f}s") # ~4-5 сек
# Многопоточный вариант
start = time.time()
threads = [
threading.Thread(target=cpu_bound_task),
threading.Thread(target=cpu_bound_task),
]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"Multi-threaded: {time.time() - start:.2f}s") # всё ещё ~4-5 сек!
Как видите, многопоточность здесь не помогает — работа не ускорилась, потому что GIL не позволяет потокам выполняться параллельно.
Когда многопоточность ПОЛЕЗНА
Многопоточность отлично подходит для I/O-bound задач — когда программа ждёт ответа от сети, диска, базы данных:
import threading
import requests
from time import time
def fetch_url(url):
response = requests.get(url)
return len(response.content)
urls = [https://example.com] * 10
# Последовательный вариант
start = time.time()
for url in urls:
fetch_url(url)
print(f"Sequential: {time.time() - start:.2f}s") # ~20 сек (2 сек на запрос)
# Многопоточный вариант
start = time.time()
threads = [threading.Thread(target=fetch_url, args=(url,)) for url in urls]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"Multi-threaded: {time.time() - start:.2f}s") # ~2 сек!
Здесь многопоточность даёт огромный выигрыш, потому что в момент ожидания сетевого ответа поток отпускает GIL, позволяя другим потокам работать.
Альтернативы
Для CPU-bound задач нужно использовать:
- multiprocessing — отдельные процессы Python, каждый со своим интерпретатором и GIL:
from multiprocessing import Pool
with Pool(processes=2) as pool:
result = pool.map(cpu_bound_task, range(2))
- Асинхронность (asyncio) — для I/O-bound:
import asyncio
import aiohttp
async def fetch_url(session, url):
async with session.get(url) as response:
return await response.text()
async def main():
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, url) for url in urls]
await asyncio.gather(*tasks)
asyncio.run(main()) # Более эффективно чем threads
- asyncio vs threads для I/O: asyncio лучше для тысяч одновременных соединений, threads достаточно для 10-100.
Итог
- I/O-bound задачи: многопоточность отлично подходит ✅
- CPU-bound задачи: используй multiprocessing или Cython ❌
- Тысячи I/O: используй asyncio вместо threads ⚡
Многопоточность в Python — не универсальное решение, но мощный инструмент для блокирующих операций ввода-вывода.