Какие есть особенности у процессов и потоков в Python?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Особенности процессов и потоков в Python
Главное отличие: Python имеет GIL (Global Interpreter Lock), который сильно влияет на работу с потоками. Процессы и потоки работают совершенно по-разному.
1. Процессы (Multiprocessing) — Истинный параллелизм
Как работают процессы?
┌─────────────────────────────────────────────┐
│ Основной процесс (PID 1000) │
│ ┌─────────────────────────────────────────┐ │
│ │ Python interpreter 1 │ │
│ │ GIL 1 (есть свой GIL!) │ │
│ └─────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────┐ │
│ │ Python interpreter 2 (отдельный!) │ │
│ │ GIL 2 (отдельный!) │ │
│ └─────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────┐ │
│ │ Python interpreter 3 │ │
│ │ GIL 3 │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
│ │ │
└────────┬┴────────┬─┘
ОС (OS)
[CPU Core 1] [CPU Core 2] [CPU Core 3]
НАСТОЯЩИЙ параллелизм!
Ключевая особенность: каждый процесс имеет СВОЙ GIL и может выполняться на разных CPU ядрах одновременно.
import multiprocessing
import time
def cpu_bound_task(n):
"""Вычисление (CPU-bound)"""
total = 0
for i in range(n):
total += i
print(f"Процесс {multiprocessing.current_process().name} закончил")
return total
if __name__ == '__main__':
# Создаём процессы
with multiprocessing.Pool(4) as pool:
start = time.time()
results = pool.map(cpu_bound_task, [50000000] * 4)
print(f"Время: {time.time() - start:.2f} сек")
# Вывод: ~5 сек (истинный параллелизм на 4 ядрах)
Преимущества:
- Истинный параллелизм на многоядерных системах
- Отлично для CPU-bound задач (вычисления, обработка данных)
- Каждый процесс независим
Недостатки:
- Большие overhead: создание процесса медленно (~10 ms)
- Память: каждый процесс копирует весь интерпретер (~50 MB)
- Общение между процессами дорого (IPC — inter-process communication)
- Сложнее синхронизировать (очереди, pipes вместо shared memory)
2. Потоки (Threading) — Кажущийся параллелизм
Как работают потоки?
┌──────────────────────────────────────────────────┐
│ Один процесс (PID 1000) │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Python interpreter (один!) │ │
│ │ GIL (ОДИН для всех потоков!) │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Thread 1 │ │ Thread 2 │ │ Thread 3 │ │ │
│ │ │ Bytecode │ │ Bytecode │ │ Bytecode │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ │ (только один выполняется в момент) │ │
│ └──────┼──────────────────────────────────────┘ │
│ │ GIL переходит между потоками │
│ └────────────────────────────────────────│
└──────────────────────────────────────────────────┘
ОС (OS)
[CPU Core 1]
(потоки НЕ параллельны, а чередуются!)
Ключевая особенность: только ОДИН поток может выполнять Python код в момент времени из-за GIL.
import threading
import time
def cpu_bound_task(n):
"""Вычисление (CPU-bound)"""
total = 0
for i in range(n):
total += i
print(f"Поток {threading.current_thread().name} закончил")
return total
# Создаём потоки
start = time.time()
threads = []
for i in range(4):
t = threading.Thread(target=cpu_bound_task, args=(50000000,))
threads.append(t)
t.start()
for t in threads:
t.join()
print(f"Время: {time.time() - start:.2f} сек")
# Вывод: ~20 сек (медленнее, чем одинарный поток из-за overhead GIL!)
Преимущества:
- Легко делиться памятью (всё в одном процессе)
- Быстро создать (microseconds vs milliseconds)
- Память: потоки очень лёгкие (~2 MB)
- Хорошо для I/O-bound задач (сеть, файлы)
Недостатки:
- GIL: нет истинного параллелизма для CPU-bound
- Потоки медленнее для вычислений
- Сложнее с ошибками (race conditions, deadlocks)
3. GIL (Global Interpreter Lock) — Враг многопоточности
Что это? Это взаимный замок (mutex), который защищает CPython VM. Только один поток может выполнять Python bytecode в момент времени.
# Пример race condition с GIL
class Counter:
def __init__(self):
self.count = 0
def increment(self):
# Это выглядит как одна операция:
self.count += 1
# Но на самом деле это ТРИ операции:
# 1. temp = self.count (GET)
# 2. temp = temp + 1 (ADD)
# 3. self.count = temp (SET)
# Между шагами GIL может переключиться на другой поток!
counter = Counter()
def worker():
for _ in range(1000000):
counter.increment()
threads = [threading.Thread(target=worker) for _ in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
print(counter.count) # Ожидаем 4000000, получим ~2500000
# Потому что GIL позволяет потокам перекрывать друг друга!
Решение: используй Lock
from threading import Lock
class Counter:
def __init__(self):
self.count = 0
self.lock = Lock()
def increment(self):
with self.lock: # Критическая секция
self.count += 1
# Теперь работает корректно: 4000000
Когда использовать что?
CPU-bound (вычисления) → Multiprocessing
import multiprocessing
def compute():
total = sum(i*i for i in range(100000000))
return total
# Хорошо: используй процессы
with multiprocessing.Pool(4) as pool:
results = pool.map(compute, range(4))
# Плохо: потоки не помогут из-за GIL
threads = [threading.Thread(target=compute) for _ in range(4)]
I/O-bound (сеть, файлы) → Threading
import threading
import requests
def fetch_url(url):
response = requests.get(url)
print(f"Загружено: {url}")
return response.text
# Хорошо: потоки идеальны (ждут I/O, отпускают GIL)
threads = [threading.Thread(target=fetch_url, args=(url,)) for url in urls]
for t in threads:
t.start()
for t in threads:
t.join()
# Или async/await ещё лучше
import asyncio
async def main():
tasks = [fetch_url_async(url) for url in urls]
await asyncio.gather(*tasks)
Особенности GIL
- GIL освобождается при I/O операциях
def io_task():
# Когда поток ждёт от сети, GIL освобождается!
response = requests.get('http://example.com') # GIL отпущен здесь
# Другой поток может работать
print(response.text) # GIL занят снова
- GIL NЕ освобождается при вычислениях
def compute_task():
total = 0
for i in range(100000000): # GIL занят весь этот цикл!
total += i
return total # GIL отпущен
- Python 3.13+ имеет "nogil" сборку (экспериментальную)
# В будущем GIL может быть отключён...
# Тогда потоки будут настоящими
На собеседовании
"В Python процессы работают параллельно на разных CPU ядрах (истинный параллелизм), поэтому используй multiprocessing для CPU-bound задач. Потоки полезны только для I/O-bound, так как GIL (Global Interpreter Lock) не позволяет им работать параллельно при вычислениях. GIL — это взаимный замок в CPython, который защищает VM и позволяет только одному потоку выполнять bytecode в момент времени."
Сравнительная таблица
| Параметр | Процессы | Потоки |
|---|---|---|
| Параллелизм | Истинный | Кажущийся (GIL) |
| GIL | Нет (свой на каждый) | Один на всех |
| Создание | Медленно (~10 ms) | Быстро (microseconds) |
| Память | Много (~50 MB) | Мало (~2 MB) |
| Общая память | Нет (IPC) | Да (shared) |
| CPU-bound | Отлично | Плохо |
| I/O-bound | Нормально | Отлично |
| Синхронизация | Очереди, Pipes | Locks, Events |