Мьютексы и семафоры

Инструменты, которыми потоки договариваются о доступе к общим данным.

Мьютекс — замок, который держит ровно один поток за раз; семафор — счётчик, разрешающий доступ ограниченному числу потоков.

Мьютекс: один ключ от комнаты

Мьютекс (mutex, от mutual exclusion) — простейший примитив взаимного исключения. Это как ключ от переговорной: кто взял (lock), тот внутри; остальные ждут, пока он не вернёт ключ (unlock). В каждый момент ключ у одного.

mutex.lock();      // взять замок (если занят — ждать)
// --- критическая секция ---
balance = balance + 50;
// ---------------------------
mutex.unlock();    // отдать замок

Пока поток держит мьютекс, гонок нет: остальные ждут у входа. Главное — не забыть unlock, иначе остальные зависнут навсегда.

Семафор: несколько одинаковых пропусков

Семафор — это счётчик с двумя операциями: wait (уменьшить счётчик, если он стал бы отрицательным — ждать) и signal (увеличить счётчик). Если начальное значение равно N, то N потоков могут пройти одновременно, а N+1-й ждёт.

  • Семафор со счётчиком 1 работает почти как мьютекс (бинарный семафор).
  • Семафор со счётчиком N ограничивает доступ N потоками — например, к пулу из N соединений.
МьютексСемафор
Сутьзамок (1 владелец)счётчик (до N доступов)
Кто освобождаеттот же поток, что захватиллюбой поток
Применениевзаимное исключениеограничение числа доступов, сигнализация

Задача «производитель-потребитель»

Классическая задача синхронизации. Есть общий буфер ограниченного размера. Производители кладут в него элементы, потребители забирают. Нужно, чтобы:

  • производитель не клал в полный буфер (ждал, пока освободится место);
  • потребитель не брал из пустого буфера (ждал, пока что-то появится);
  • доступ к буферу был защищён от гонок.

В реальном решении используют два семафора (счётчик пустых мест и счётчик занятых) и мьютекс на сам буфер. Смоделируем поведение буфера на последовательности событий.

from collections import deque

def simulate(events, capacity):
    buffer = deque()
    produced = 0
    for ev in events:
        if ev == "P":  # производитель
            if len(buffer) < capacity:
                produced += 1
                buffer.append(produced)
                print(f"Производитель положил {produced}, в буфере {len(buffer)}")
            else:
                print(f"Буфер полон ({capacity}) — производитель ЖДЁТ")
        elif ev == "C":  # потребитель
            if buffer:
                item = buffer.popleft()
                print(f"Потребитель взял {item}, в буфере {len(buffer)}")
            else:
                print("Буфер пуст — потребитель ЖДЁТ")

simulate(["P", "P", "P", "C", "P", "C", "C", "C"], capacity=2)

Вывод:

Производитель положил 1, в буфере 1
Производитель положил 2, в буфере 2
Буфер полон (2) — производитель ЖДЁТ
Потребитель взял 1, в буфере 1
Производитель положил 3, в буфере 2
Потребитель взял 2, в буфере 1
Потребитель взял 3, в буфере 0
Буфер пуст — потребитель ЖДЁТ

Видно, как семафоры заставляют производителя ждать у полного буфера, а потребителя — у пустого. Это и есть синхронизация в действии.

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

  • Забыли unlock. Мьютекс остаётся захваченным — остальные зависают.
  • Защитили не все обращения. Достаточно одного незащищённого доступа к общим данным, чтобы гонка вернулась.
  • Слишком большая критическая секция. Под замком держат больше, чем нужно — теряется параллелизм.

Итог

  • Мьютекс — замок для взаимного исключения: один владелец за раз.
  • Семафор — счётчик, пропускающий до N потоков; бинарный семафор ≈ мьютекс.
  • Мьютекс освобождает тот же поток, семафор — любой.
  • Задача производитель-потребитель решается двумя семафорами и мьютексом.
  • Типичные ошибки: забытый unlock, незащищённый доступ, слишком крупная критическая секция.
Проверьте себя
1. В чём ключевое отличие семафора от мьютекса?
AСемафор работает только в режиме ядра
BСемафор — счётчик, пропускающий до N потоков, а мьютекс — замок с одним владельцем
CМьютекс быстрее семафора в любом случае
DСемафор нельзя освободить
2. Что должно произойти, когда производитель пытается положить элемент в полный буфер?
AСтарый элемент перезаписывается
BПроизводитель ждёт, пока освободится место
CБуфер автоматически расширяется
DПотребитель удаляется
3. Какая из ошибок синхронизации приведёт к зависанию остальных потоков?
AСлишком маленькая критическая секция
BЗабытый вызов unlock мьютекса
CИспользование семафора вместо мьютекса
DЧтение общих данных под замком
Поддержать проект