Семафоры и условные переменные

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

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

Мьютекс — это семафор со счётчиком 1: один вошёл, остальные ждут. Но иногда нужно пускать не одного, а, скажем, троих — например, ограничить число одновременных запросов к API.

Семафор: пул разрешений

Семафор хранит число доступных «разрешений». acquire уменьшает счётчик (если он 0 — поток ждёт), release увеличивает. Это удобно для ограничения нагрузки.

import threading

# не больше 3 потоков одновременно качают
sem = threading.Semaphore(3)

def download(name):
    with sem:
        print(f"{name} качает")
        # ... работа ...

for i in range(10):
    threading.Thread(target=download, args=(f"job{i}",)).start()

Смоделируем счётчик разрешений без потоков:

permits = 3
queue = ["A", "B", "C", "D", "E"]

for job in queue:
    if permits > 0:
        permits -= 1
        print(f"{job}: вошёл, осталось разрешений {permits}")
        permits += 1   # сразу отдал, упрощённо
    else:
        print(f"{job}: ждёт")

Вывод:

A: вошёл, осталось разрешений 2
B: вошёл, осталось разрешений 2
C: вошёл, осталось разрешений 2
D: вошёл, осталось разрешений 2
E: вошёл, осталось разрешений 2

Условная переменная: ждать события

Иногда поток не может работать, пока не наступит условие («в очереди появился элемент»). Крутиться в цикле и проверять — расточительно. Условная переменная (Condition) позволяет потоку уснуть на wait() и проснуться, когда другой поток вызовет notify().

import threading

cond = threading.Condition()
items = []

def consumer():
    with cond:
        while not items:
            cond.wait()        # спим, пока не разбудят
        print("взял", items.pop())

def producer():
    with cond:
        items.append("данные")
        cond.notify()          # будим одного потребителя

Ключевая деталь — проверка условия в цикле while, а не if: поток мог проснуться, но условие к моменту захвата ключа снова стало ложным.

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

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

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

  • Проверять условие через if вместо while. Из-за ложных пробуждений и гонок условие нужно перепроверять в цикле.
  • Звать wait() без удержания блокировки. Условная переменная требует, чтобы блокировка была захвачена.
  • Забыть notify. Если никто не разбудит спящего, он будет ждать вечно.

Итог

  • Семафор пускает внутрь до N потоков — удобно ограничивать нагрузку.
  • Мьютекс — частный случай семафора со счётчиком 1.
  • Условная переменная усыпляет поток до сигнала вместо активного ожидания.
  • Условие всегда перепроверяйте в цикле while.
Проверьте себя
1. Чем семафор отличается от обычного мьютекса?
AСемафор быстрее, но небезопасен
BСемафор позволяет внутрь до N потоков, а мьютекс — только одного
CСемафор не использует блокировок
DОни идентичны
2. Зачем нужна условная переменная?
AЧтобы ускорить вычисления на CPU
BЧтобы поток уснул и проснулся по сигналу вместо активного ожидания в цикле
CЧтобы создать новый процесс
DЧтобы отключить планировщик
3. Почему условие пробуждения проверяют в цикле while, а не через if?
AТак короче код
BПоток мог проснуться, но условие снова стало ложным — его нужно перепроверить
Cwhile работает быстрее if
Dif нельзя использовать в потоках