Критическая секция и взаимное исключение (mutex/lock)

Лекарство от гонок: пускать в опасный участок кода только один поток за раз.

Критическая секция — участок кода, обращающийся к общему ресурсу, который в любой момент должен выполнять не более одного потока; взаимное исключение (mutual exclusion) — гарантия этого свойства.

Раз гонка возникает при одновременном доступе к общим данным, решение очевидно: запретить одновременность именно в этом месте. Инструмент для этого — мьютекс (mutex, от mutual exclusion), он же блокировка (Lock).

Как работает мьютекс

Мьютекс похож на ключ от переговорной комнаты. Поток, который хочет войти в критическую секцию, должен сначала захватить блокировку (acquire). Пока ключ у него, остальные ждут. Закончив, поток освобождает блокировку (release), и ключ забирает следующий.

lock = Lock()

lock.acquire()   # взял ключ
counter += 1     # критическая секция: я тут один
lock.release()   # отдал ключ

В Python безопаснее использовать контекстный менеджер with — он сам вызовет release, даже если внутри возникнет исключение:

import threading

lock = threading.Lock()
counter = 0

def increment():
    global counter
    with lock:          # acquire на входе, release на выходе
        counter += 1

threads = [threading.Thread(target=increment) for _ in range(1000)]
for t in threads: t.start()
for t in threads: t.join()
print(counter)   # теперь стабильно 1000

Этот код использует реальные потоки, поэтому в браузере не запускается. Зато логику «один ключ — одна комната» легко смоделировать:

lock_taken = False
counter = 0

def enter_critical(name):
    global lock_taken, counter
    if lock_taken:
        print(f"{name}: жду, ключ занят")
        return
    lock_taken = True
    counter += 1            # критическая секция
    print(f"{name}: вошёл, counter = {counter}")
    lock_taken = False

enter_critical("A")
enter_critical("B")

Вывод:

A: вошёл, counter = 1
B: вошёл, counter = 2

Свойства правильного взаимного исключения

  • Безопасность: в критической секции максимум один поток.
  • Прогресс: если секция свободна, кто-то из желающих обязательно войдёт.
  • Без голодания: ни один поток не ждёт ключ вечно.

Как работает под капотом

Внутри мьютекс опирается на атомарные аппаратные инструкции вроде compare-and-swap, которые процессор гарантированно выполняет целиком, без вклинивания. Если ключ занят, поток обычно переходит в состояние «заблокирован», и планировщик не тратит на него кванты, пока ключ не освободится. Это эффективнее, чем «крутиться» в цикле и постоянно проверять.

Частые ошибки

  • Забыть release. Если поток взял ключ и не отдал (например, из-за исключения), остальные зависнут навсегда. Используйте with.
  • Слишком широкая критическая секция. Если держать блокировку дольше нужного, потоки выстраиваются в очередь и параллелизм исчезает.
  • Разные блокировки на один ресурс. Если два потока защищают одни данные разными мьютексами, защиты нет.

Итог

  • Критическая секция — код, который должен выполнять один поток за раз.
  • Мьютекс даёт взаимное исключение через acquire/release.
  • В Python используйте with lock: — он надёжно освобождает блокировку.
  • Держите критическую секцию как можно короче.
Проверьте себя
1. Что такое критическая секция?
AСамая быстрая часть программы
BУчасток кода с общим ресурсом, который должен выполнять только один поток за раз
CБлок обработки ошибок
DГлавная функция программы
2. Почему в Python предпочтительнее with lock: вместо ручных acquire/release?
Awith работает быстрее процессора
Bwith гарантированно вызовет release даже при исключении
CРучной acquire запрещён
Dwith создаёт новый поток
3. Чем плоха слишком широкая (длинная) критическая секция?
AОна вызывает гонку
BПотоки выстраиваются в очередь и теряется параллелизм
CОна уменьшает размер кода
DОна отключает мьютекс