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

Что лучше использовать для выполнения вычислительно затратных операций в 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.