В чём разница между многопроцессностью и многопоточностью?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Многопроцессность vs Многопоточность в Python
Это фундаментальное различие в моделях конкурентности, и для Python оно имеет особенное значение из-за GIL (Global Interpreter Lock).
Основные определения
Многопоточность (multithreading)
Несколько потоков работают в одном процессе, совместно используя одно адресное пространство памяти. Контекстное переключение между потоками происходит очень быстро (на уровне операционной системы).
import threading
import time
def worker(name):
for i in range(3):
print(f"Worker {name}: {i}")
time.sleep(1)
# Создание потоков
threads = []
for i in range(2):
t = threading.Thread(target=worker, args=(f"Thread-{i}",))
threads.append(t)
t.start()
# Ожидание завершения
for t in threads:
t.join()
print("Все потоки завершены")
Потоки работают в одном процессе, поэтому они могут легко делиться данными:
import threading
shared_value = 0
lock = threading.Lock()
def increment():
global shared_value
for _ in range(1000000):
with lock:
shared_value += 1
t1 = threading.Thread(target=increment)
t2 = threading.Thread(target=increment)
t1.start()
t2.start()
t1.join()
t2.join()
print(f"Result: {shared_value}") # 2000000
Многопроцессность (multiprocessing)
Несколько независимых процессов с собственным адресным пространством. Каждый процесс — полностью отдельный экземпляр интерпретатора Python с собственным GIL.
import multiprocessing
import time
def worker(name):
for i in range(3):
print(f"Worker {name}: {i}")
time.sleep(1)
if __name__ == '__main__':
processes = []
for i in range(2):
p = multiprocessing.Process(target=worker, args=(f"Process-{i}",))
processes.append(p)
p.start()
for p in processes:
p.join()
print("Все процессы завершены")
Даже разделяемые переменные требуют специальных механизмов (Queue, Pipe, Manager):
import multiprocessing
def worker(q):
q.put(42) # Отправить значение в очередь
if __name__ == '__main__':
queue = multiprocessing.Queue()
p = multiprocessing.Process(target=worker, args=(queue,))
p.start()
value = queue.get() # Получить значение
print(f"Got: {value}")
p.join()
Сравнительная таблица
| Аспект | Многопоточность | Многопроцессность |
|---|---|---|
| Адресное пространство | Общее | Отдельное у каждого |
| Память | Экономна | Требует больше памяти |
| Создание | Быстро (< 1мс) | Медленно (10мс+) |
| Синхронизация | Легко (shared memory) | Сложно (IPC) |
| GIL в Python | МЕШАЕТ для CPU-bound | НЕ МЕШАЕТ — обходит GIL |
| Отладка | Сложнее (race conditions) | Проще (изоляция) |
| Надёжность | Одна ошибка — весь процесс | Крах одного — остальные живы |
Почему в Python это так важно: Global Interpreter Lock (GIL)
Python (CPython) имеет мьютекс GIL, который позволяет только одному потоку одновременно выполнять код на Python. Это означает, что многопоточность НЕ даёт параллелизма для CPU-bound задач.
CPU-bound пример (вычисления):
import threading
import time
def cpu_intensive():
total = 0
for i in range(100000000):
total += i
return total
# Однопоточный вариант
start = time.time()
cpu_intensive()
cpu_intensive()
print(f"Sequential: {time.time() - start:.2f}s") # ~3.2s
# Многопоточный вариант (из-за GIL работает медленнее!)
start = time.time()
t1 = threading.Thread(target=cpu_intensive)
t2 = threading.Thread(target=cpu_intensive)
t1.start()
t2.start()
t1.join()
t2.join()
print(f"Threading: {time.time() - start:.2f}s") # ~4.0s (МЕДЛЕННЕЕ!)
# Многопроцессный вариант (настоящий параллелизм)
start = time.time()
p1 = multiprocessing.Process(target=cpu_intensive)
p2 = multiprocessing.Process(target=cpu_intensive)
p1.start()
p2.start()
p1.join()
p2.join()
print(f"Multiprocessing: {time.time() - start:.2f}s") # ~1.8s (в 2 раза быстрее!)
I/O-bound пример (ввод-вывод):
import threading
import asyncio
import httpx
import time
# Многопоточный вариант (работает нормально)
start = time.time()
threads = []
for _ in range(10):
t = threading.Thread(target=lambda: httpx.get("https://api.github.com"))
threads.append(t)
t.start()
for t in threads:
t.join()
print(f"Threading (I/O): {time.time() - start:.2f}s") # ~1.5s (хорошо!)
# Асинхронный вариант (ещё лучше)
async def fetch_all():
async with httpx.AsyncClient() as client:
tasks = [client.get("https://api.github.com") for _ in range(10)]
await asyncio.gather(*tasks)
start = time.time()
asyncio.run(fetch_all())
print(f"Async (I/O): {time.time() - start:.2f}s") # ~1.2s (ещё быстрее)
Практические рекомендации
Используй многопоточность для:
- I/O-bound задач (сетевые запросы, работа с файлами)
- Отзывчивого пользовательского интерфейса (UI остаётся ответственным)
- Когда нужно делиться памятью между параллельными задачами
from concurrent.futures import ThreadPoolExecutor
import requests
with ThreadPoolExecutor(max_workers=10) as executor:
urls = [f"https://api.github.com/users/user{i}" for i in range(100)]
results = executor.map(requests.get, urls)
Используй многопроцессность для:
- CPU-bound задач (вычисления, обработка данных)
- Когда нужна надёжность (крах одного процесса не влияет на остальные)
- Когда требуется истинный параллелизм на нескольких ядрах
from concurrent.futures import ProcessPoolExecutor
def heavy_computation(n):
return sum(i*i for i in range(n))
with ProcessPoolExecutor(max_workers=4) as executor:
results = executor.map(heavy_computation, [100000000] * 4)
Используй асинхронность для:
- Максимально масштабируемых I/O операций (десятки тысяч одновременных соединений)
- Микросервисов и API серверов
- Когда нужно обработать много параллельных задач эффективно
import asyncio
import aiohttp
async def fetch_all(urls):
async with aiohttp.ClientSession() as session:
tasks = [session.get(url) for url in urls]
return await asyncio.gather(*tasks)
asyncio.run(fetch_all([...]))
Выводы
- GIL в Python делает многопоточность неэффективной для CPU-bound задач
- Для I/O-bound задач многопоточность и асинхронность работают хорошо
- Многопроцессность обходит GIL, но требует больше памяти и сложнее в синхронизации
- Асинхронность (async/await) — золотой стандарт для I/O-bound приложений в Python