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

Как в мультипотоке работает обращение к памяти?

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.

Как в мультипотоке работает обращение к памяти? | PrepBro