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

Какие есть особенности у процессов и потоков в Python?

2.0 Middle🔥 171 комментариев
#Python Core#Асинхронность и многопоточность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Особенности процессов и потоков в 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

  1. GIL освобождается при I/O операциях
def io_task():
    # Когда поток ждёт от сети, GIL освобождается!
    response = requests.get('http://example.com')  # GIL отпущен здесь
    # Другой поток может работать
    print(response.text)  # GIL занят снова
  1. GIL NЕ освобождается при вычислениях
def compute_task():
    total = 0
    for i in range(100000000):  # GIL занят весь этот цикл!
        total += i
    return total  # GIL отпущен
  1. 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НормальноОтлично
СинхронизацияОчереди, PipesLocks, Events