← Назад к вопросам
Что лучше использовать для выполнения вычислительно затратных операций в Python: многопоточность или многопроцессность?
2.0 Middle🔥 231 комментариев
#Python Core
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Многопоточность vs Многопроцессность в Python
Это критический выбор для производительности. Ответ: это зависит от задачи. Многопоточность хороша для I/O операций, многопроцессность для CPU-bound операций. Главная причина — GIL (Global Interpreter Lock) в CPython.
Перво-причина: GIL (Global Interpreter Lock)
GIL — это мьютекс в CPython, который позволяет только одному потоку выполнять Python код одновременно.
# ❌ Многопоточность для CPU-bound работы БЕСПОЛЕЗНА
import threading
import time
def cpu_bound_task(n):
"""Затратная вычислительная операция"""
result = 0
for i in range(n):
result += i ** 2
return result
# Однопоточное выполнение
start = time.time()
cpu_bound_task(100_000_000)
cpu_bound_task(100_000_000)
print(f"Single-threaded: {time.time() - start:.2f}s") # ~2.5s
# Многопоточное выполнение (с GIL)
start = time.time()
threads = [
threading.Thread(target=cpu_bound_task, args=(100_000_000,)),
threading.Thread(target=cpu_bound_task, args=(100_000_000,))
]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"Multi-threaded: {time.time() - start:.2f}s") # ~2.5s (не быстрее!)
# GIL не даёт параллелизма!
Правило 1: I/O-Bound операции → Многопоточность
import threading
import time
import requests
def fetch_url(url):
"""I/O операция: большое время ожидания сети"""
response = requests.get(url)
return len(response.content)
# ❌ Последовательное выполнение
start = time.time()
for url in ["https://example.com"] * 10:
fetch_url(url)
print(f"Sequential: {time.time() - start:.2f}s") # ~10s (долго)
# ✅ Многопоточное (пока один ждёт сети, другой работает)
start = time.time()
threads = []
for url in ["https://example.com"] * 10:
t = threading.Thread(target=fetch_url, args=(url,))
t.start()
threads.append(t)
for t in threads:
t.join()
print(f"Multi-threaded: {time.time() - start:.2f}s") # ~1s (быстро!)
Правило 2: CPU-Bound операции → Многопроцессность
from multiprocessing import Pool
import time
def cpu_bound_task(n):
"""Затратная вычислительная операция"""
result = 0
for i in range(n):
result += i ** 2
return result
# ❌ Многопоточность (GIL не даёт параллелизма)
import threading
start = time.time()
threads = [
threading.Thread(target=cpu_bound_task, args=(100_000_000,)),
threading.Thread(target=cpu_bound_task, args=(100_000_000,))
]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"Multi-threaded: {time.time() - start:.2f}s") # ~2.5s
# ✅ Многопроцессность (каждый процесс свой GIL)
start = time.time()
with Pool(processes=2) as pool:
results = pool.map(
cpu_bound_task,
[100_000_000, 100_000_000]
)
print(f"Multi-process: {time.time() - start:.2f}s") # ~1.3s (2x ускорение!)
Многопоточность: параллелизм в I/O
import threading
from concurrent.futures import ThreadPoolExecutor
import time
def make_request(url):
"""I/O операция"""
time.sleep(1) # Имитируем сетевой запрос
return f"Response from {url}"
# Способ 1: threading.Thread
start = time.time()
threads = []
for i in range(10):
t = threading.Thread(
target=make_request,
args=(f"url{i}",)
)
t.start()
threads.append(t)
for t in threads:
t.join()
print(f"Threads: {time.time() - start:.2f}s") # ~1s (параллелизм)
# Способ 2: ThreadPoolExecutor (лучше)
start = time.time()
with ThreadPoolExecutor(max_workers=10) as executor:
futures = [
executor.submit(make_request, f"url{i}")
for i in range(10)
]
results = [f.result() for f in futures]
print(f"ThreadPool: {time.time() - start:.2f}s") # ~1s
Многопроцессность: истинный параллелизм
from multiprocessing import Process, Pool
import time
def cpu_intensive(n):
"""Каждый процесс имеет свой GIL"""
total = 0
for i in range(n):
total += i ** 2
return total
# Способ 1: Process (низкоуровневый)
start = time.time()
p1 = Process(target=cpu_intensive, args=(100_000_000,))
p2 = Process(target=cpu_intensive, args=(100_000_000,))
p1.start()
p2.start()
p1.join()
p2.join()
print(f"Processes: {time.time() - start:.2f}s") # ~1.3s
# Способ 2: Pool (удобнее)
start = time.time()
with Pool(processes=2) as pool:
results = pool.map(
cpu_intensive,
[100_000_000, 100_000_000]
)
print(f"Pool: {time.time() - start:.2f}s") # ~1.3s
Проблема многопроцессности: overhead
from multiprocessing import Pool
import time
def quick_task(x):
"""Быстрая операция"""
return x ** 2
# Последовательное выполнение (быстро для мелких задач)
start = time.time()
results = [quick_task(i) for i in range(10000)]
print(f"Sequential: {time.time() - start:.4f}s") # 0.005s
# Многопроцессность (медленнее из-за overhead)
start = time.time()
with Pool(processes=4) as pool:
results = pool.map(quick_task, range(10000))
print(f"Multi-process: {time.time() - start:.4f}s") # 0.3s (медленнее!)
# Вывод: используй multiprocessing только для ЗАТРАТНЫХ операций
Async/await: альтернатива для I/O
import asyncio
import time
async def fetch_url(url):
"""Асинхронная I/O операция"""
await asyncio.sleep(1) # Имитируем сетевой запрос
return f"Response from {url}"
async def main():
"""Параллельное выполнение асинхронных задач"""
# Создаём 10 задач
tasks = [
fetch_url(f"url{i}")
for i in range(10)
]
# Выполняем все одновременно
results = await asyncio.gather(*tasks)
return results
# Выполнение
start = time.time()
results = asyncio.run(main())
print(f"Async: {time.time() - start:.2f}s") # ~1s (однопоточно!)
# Преимущества:
# - Нет потокообслуживания overhead
# - Масштабируется на 10000+ задач
# - Проще debug
Сравнение подходов
┌──────────────────┬──────────────┬────────────────┬──────────────┐
│ Задача │ Threading │ Multiprocessing│ Async/Await │
├──────────────────┼──────────────┼────────────────┼──────────────┤
│ I/O-bound (сеть) │ ✅ Хорошо │ ❌ Плохо │ ✅✅ Лучше │
│ I/O-bound (диск) │ ✅ Хорошо │ ❌ Плохо │ ✅ Хорошо │
│ CPU-bound │ ❌ GIL │ ✅ Хорошо │ ❌ Плохо │
│ Масштабируемость │ ~100 потоков │ ~кол-во ядер │ ~10000 задач │
│ Сложность │ Средняя │ Высокая │ Средняя │
│ Разделение памяти│ Легко (shared)│ Сложно (copy) │ Легко │
└──────────────────┴──────────────┴────────────────┴──────────────┘
Практический пример: веб-скрейпер
# ✅ Используй ThreadPoolExecutor для HTTP запросов
from concurrent.futures import ThreadPoolExecutor
import requests
def scrape_page(url):
response = requests.get(url)
# Обработка страницы
return len(response.content)
urls = [f"https://example.com/page{i}" for i in range(100)]
with ThreadPoolExecutor(max_workers=10) as executor:
results = list(executor.map(scrape_page, urls))
print(f"Total pages: {len(results)}")
Практический пример: обработка данных
# ✅ Используй Pool для CPU-bound обработки
from multiprocessing import Pool
import numpy as np
def process_chunk(data):
"""CPU-интенсивная операция"""
return np.mean(data) ** 2
large_data = np.random.rand(1000000)
chunks = np.array_split(large_data, 4)
with Pool(processes=4) as pool:
results = pool.map(process_chunk, chunks)
final_result = sum(results)
Заключение
Правило:
- I/O-bound (сеть, диск) → Threading или Async/Await
- CPU-bound (вычисления) → Multiprocessing
- Много I/O задач (10000+) → Async/Await
- Много CPU задач → Multiprocessing
- Смешанные задачи → Комбинируй: async + ThreadPoolExecutor для I/O, Pool для CPU
Забудь про GIL, выбирай правильный инструмент для задачи. Python не идеален для CPU-bound параллелизма, но отлично справляется с I/O благодаря threading и async.