Мьютексы и семафоры
Инструменты, которыми потоки договариваются о доступе к общим данным.
Мьютекс — замок, который держит ровно один поток за раз; семафор — счётчик, разрешающий доступ ограниченному числу потоков.
Мьютекс: один ключ от комнаты
Мьютекс (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, незащищённый доступ, слишком крупная критическая секция.