← Назад к вопросам
Как в мультипотоке работает обращение к памяти?
2.0 Middle🔥 131 комментариев
#Soft Skills
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Доступ к памяти в многопоточности
Это сложная тема, требующая понимания железа, ОС и Python. Разберу от простого к сложному.
Уровень 1: Что такое память в многопотоке
# Разные потоки видят ОДНУ И ТУ ЖЕ память
shared_counter = 0
def increment():
global shared_counter
for _ in range(1000000):
shared_counter += 1
# Если запустим 2 потока:
# Thread 1 и Thread 2 видят ОДНУ переменную shared_counter
# Но операция += НЕ атомарная!
Уровень 2: Проблема race condition
# На уровне процессора это происходит так:
# shared_counter = 5
# Thread 1: # Thread 2:
# READ: r1 = 5 # (выполняется одновременно)
# (контекстный переход) READ: r2 = 5
# ADD: r1 = 6 # ADD: r2 = 6
# WRITE: 6 # WRITE: 6
# Результат: 6, а не 7!
# Теряется инкремент от одного потока
Уровень 3: GIL в Python
import threading
import time
shared_list = []
def add_items():
for i in range(1000):
shared_list.append(i) # Безопасно благодаря GIL
threads = [threading.Thread(target=add_items) for _ in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
print(len(shared_list)) # Всегда 4000, GIL защищает
GIL (Global Interpreter Lock):
- Только один поток может исполнять Python код одновременно
- Автоматически защищает многие операции
- НО — не защищает сложные операции!
# Даже с GIL это не безопасно!
counter = 0
def increment():
global counter
temp = counter # READ
temp += 1 # compute (GIL может отпуститься)
counter = temp # WRITE
# Два потока могут прерваться между READ и WRITE
Уровень 4: Кэши и когерентность памяти
Вот где реально сложно. На современном процессоре:
CPU1 (Core 1) CPU2 (Core 2)
L1 кэш L1 кэш
| |
L2 кэш (shared) L2 кэш
|___________________|
L3 кэш
|
Память
Проблема: False Sharing
# Два ядра работают с разными переменными,
# но они в одной cache line (64 байта)
class Counter:
def __init__(self):
self.counter1 = 0 # Адрес: 0x1000
self.counter2 = 0 # Адрес: 0x1008 (в одной cache line!)
# CPU 1 обновляет counter1 → инвалидирует весь cache line на CPU 2
# CPU 2 должна перечитать, хотя counter2 не менялась
# Результат: медленно!
Решение: padding
class Counter:
def __init__(self):
self.counter1 = 0
self.padding = [0] * 7 # 56 байт → следующая переменная на новой line
self.counter2 = 0
Уровень 5: Memory Barriers (Barriers)
import threading
# Без синхронизации — результат непредсказуем
x = 0
y = 0
def thread1():
global x, y
x = 1 # Если не barrier — может быть видно раньше y
y = 2
def thread2():
print(x, y) # Может быть: (0, 0), (1, 0), (1, 2) — в любом порядке!
from threading import Lock
lock = Lock()
def thread1_safe():
with lock:
x = 1
y = 2
# Fence: гарантия, что оба write'а видны другим потокам
def thread2_safe():
with lock:
print(x, y) # Гарантированно (1, 2)
Уровень 6: Модели консистентности памяти
Процессоры разных производителей дают разные гарантии:
# x86 (Intel, AMD) — Strong Memory Model
# Почти любой порядок операций гарантирован
# Исключение: Store → Load может переупорядочиться
# ARM (мобильные, некоторые серверы) — Weak Memory Model
# Практически ВСЕ переупорядочения возможны
# Нужны явные barriers
# Python абстрагирует это, но важно знать для низкоуровневого кода
Практический подход в Python
1. Используй Lock для критической секции
from threading import Lock
class BankAccount:
def __init__(self, balance):
self.balance = balance
self.lock = Lock()
def withdraw(self, amount):
with self.lock: # Critical section
if self.balance >= amount:
self.balance -= amount # Безопасно
return True
return False
2. Используй Queue для обмена данными
from queue import Queue
import threading
q = Queue() # Thread-safe
def producer():
for i in range(100):
q.put(i) # Безопасно
def consumer():
while True:
item = q.get() # Блокирующее, безопасное
process(item)
3. Используй threading.Event
event = threading.Event()
def worker():
event.wait() # Блокируется до signal
do_work()
# Основной поток
event.set() # Сигнализирует всем workers
4. RLock для рекурсивного доступа
from threading import RLock
lock = RLock()
def function_a():
with lock:
result = function_b() # Может использовать тот же lock
return result
def function_b():
with lock: # RLock позволяет переиспользовать
pass
Почему многопоток плох для I/O-bound в Python
# GIL отпускается только на I/O операциях
# Когда один поток ждет сеть, другой может работать
# Но контекстные переключения дорогие
import time
# Многопоток (медленно)
def slow_request():
time.sleep(1) # Имитация I/O
start = time.time()
for _ in range(4):
slow_request() # 4 секунды
print(f"Sequential: {time.time() - start}") # ~4s
# Многопоток
import threading
start = time.time()
threads = [threading.Thread(target=slow_request) for _ in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"Threaded: {time.time() - start}") # ~1s (параллельное ожидание)
# Asyncio (лучше для I/O)
import asyncio
async def async_request():
await asyncio.sleep(1)
async def main():
await asyncio.gather(*[async_request() for _ in range(4)])
start = time.time()
asyncio.run(main())
print(f"Async: {time.time() - start}") # ~1s (эффективнее)
Итоговая таблица
| Операция | Потокобезопасна? | Защита |
|---|---|---|
| list.append() | ДА (GIL) | - |
| list[i] = x | ДА (GIL) | - |
| counter += 1 | НЕТ | Lock |
| dict[key] = val | ДА (GIL) | - |
| custom объект | ? | Lock обязателен |
Правило: Если не уверен — используй Lock или Queue.