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

Какие существуют проблемы с multithreading?

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

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

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

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

Проблемы с Multithreading

Многопоточность в Python — мощный инструмент, но полная проблем. Понимание этих проблем критично для написания надёжного кода.

1. GIL (Global Interpreter Lock)

В CPython существует единая блокировка, которая позволяет только одному потоку выполнять Python код в момент времени. Это огромное ограничение для CPU-bound задач.

import threading
import time

def cpu_bound_task():
    total = 0
    for i in range(50_000_000):
        total += i
    return total

# Однопоточный вариант
start = time.time()
cpu_bound_task()
cpu_bound_task()
print(f"Одна нить: {time.time() - start:.2f}s")
# Результат: ~5 секунд

# Двухпоточный вариант
start = time.time()
threads = []
for _ in range(2):
    t = threading.Thread(target=cpu_bound_task)
    threads.append(t)
    t.start()
for t in threads:
    t.join()
print(f"Две нити: {time.time() - start:.2f}s")
# Результат: ~10 секунд (медленнее!)
# Потому что GIL переключает потоки, тратя время

Почему GIL существует? Управление памятью в CPython (reference counting) не потокобезопасно. GIL — самый простой способ решить эту проблему.

Решение: используй multiprocessing для CPU-bound задач или PyPy/Jython без GIL.

2. Race Conditions

Когда несколько потоков одновременно обращаются к одним данным, может произойти непредсказуемый результат.

import threading

counter = 0

def increment():
    global counter
    for _ in range(1_000_000):
        counter += 1  # Операция: read → modify → write (3 шага!)

threads = [threading.Thread(target=increment) for _ in range(2)]
for t in threads:
    t.start()
for t in threads:
    t.join()

print(counter)  # Ожидаем 2_000_000, но получим меньше (например, 1_847_293)
# Потому что потоки мешают друг другу

Объяснение: операция counter += 1 состоит из трёх команд CPU:

Поток 1: LOAD counter (значение: 0)
Поток 2:    LOAD counter (значение: 0)
Поток 1:    ADD 1
Поток 2:         ADD 1
Поток 1:    STORE (1)
Поток 2:         STORE (1)

Результат: 1 вместо 2

Решение: используй Lock (мьютекс)

import threading

counter = 0
lock = threading.Lock()

def increment():
    global counter
    for _ in range(1_000_000):
        with lock:  # Только один поток может быть здесь одновременно
            counter += 1

threads = [threading.Thread(target=increment) for _ in range(2)]
for t in threads:
    t.start()
for t in threads:
    t.join()

print(counter)  # 2_000_000 — правильно!

3. Deadlock (Взаимная Блокировка)

Когда потоки ждут друг друга, заблокировав ресурсы, происходит deadlock.

import threading
import time

lock1 = threading.Lock()
lock2 = threading.Lock()

def thread1_func():
    with lock1:  # Захватил lock1
        print("Thread 1: захватил lock1")
        time.sleep(0.1)  # Даём время thread2 захватить lock2
        with lock2:  # Попытка захватить lock2
            print("Thread 1: захватил lock2")

def thread2_func():
    with lock2:  # Захватил lock2
        print("Thread 2: захватил lock2")
        time.sleep(0.1)
        with lock1:  # Попытка захватить lock1
            print("Thread 2: захватил lock1")

t1 = threading.Thread(target=thread1_func)
t2 = threading.Thread(target=thread2_func)
t1.start()
t2.start()
t1.join()  # Висит навечно! Deadlock.
t2.join()

Решение: всегда захватывай блокировки в одном порядке

def thread_func():
    with lock1:  # Первый
        with lock2:  # Всегда второй
            # Безопасно
            pass

4. Priority Inversion

Высокоприоритетный поток ждёт, пока низкоприоритетный закончит. Редко в Python, но может быть.

5. Сложность отладки

Ошибки в многопоточном коде воспроизводятся случайно и непредсказуемо.

# Этот код может работать 1000 раз, а потом упасть
def unreliable_code():
    shared_list = []
    
    def append_items():
        for i in range(1000):
            shared_list.append(i)  # Race condition!
    
    def read_items():
        for i in range(1000):
            _ = len(shared_list)
    
    t1 = threading.Thread(target=append_items)
    t2 = threading.Thread(target=read_items)
    t1.start()
    t2.start()
    t1.join()
    t2.join()

6. Memory Overhead

Каждый поток занимает память (~8MB на Linux), потребление растёт экспоненциально.

import threading

def worker():
    import time
    time.sleep(10)

# 1000 потоков = ~8GB памяти
for _ in range(1000):
    t = threading.Thread(target=worker)
    t.start()

7. Context Switching Overhead

Перемена потоков требует сохранения/восстановления состояния, это дорого.

Когда Threading безопасен

I/O-bound операции (сеть, файлы) — потоки уходят в ожидание, не конкурируя за CPU.

import threading
import requests

def fetch_url(url):
    response = requests.get(url)  # Поток ждёт, не занимает CPU
    return response.status_code

threads = [
    threading.Thread(target=fetch_url, args=(url,))
    for url in ['http://example.com', 'http://google.com']
]

for t in threads:
    t.start()
for t in threads:
    t.join()

Альтернативы

  • asyncio — для I/O-bound без overhead потоков
  • multiprocessing — для CPU-bound обходя GIL
  • concurrent.futures — простой API для потоков

Мультипоточность в Python требует глубокого понимания синхронизации и очень осторожного подхода.

Какие существуют проблемы с multithreading? | PrepBro