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