В чем отличие семафора от мьютекса
Что проверяют
Понимаете ли вы разницу в назначении, а не только «один пускает одного, другой нескольких». Для Python есть второй слой: зачем синхронизация при GIL и какие примитивы дают threading и asyncio. Следом часто просят ограничить число одновременных запросов к внешнему API.
Как отвечать
Разведите понятия:
- мьютекс: взаимное исключение. В критической секции один поток, и по классическому определению освобождает замок тот, кто его захватил;
- семафор: счётчик разрешений.
acquire()уменьшает его и блокирует на нуле,release()увеличивает. Он ограничивает число одновременных участников и годится для сигнализации: один поток освобождает, другой ждёт.
В Python это:
threading.Lock: простой замок. Владельца он не проверяет, отпустить его может любой поток;threading.RLock: реентерабельный, знает владельца, повторный захват тем же потоком не блокирует;threading.SemaphoreиBoundedSemaphore: второй бросаетValueError, еслиrelease()вызвали больше раз, чемacquire(), и так ловит ошибки логики;asyncio.Lockиasyncio.Semaphore: то же для корутин.asyncio.Semaphore(10)вокруг запросов черезhttpxилиaiohttpдержит не больше десяти одновременных вызовов.
Захват пишите через with lock: или async with sem:, тогда исключение внутри блока не оставит замок закрытым.
Про GIL скажите прямо: он не делает код потокобезопасным. counter += 1 не атомарна, это несколько операций байткода, и переключение потока между ними теряет инкременты. В free-threaded сборках без GIL, которые появились в Python 3.13, гонки проявляются ещё чаще.
Где спотыкаются
Говорят, что семафор со значением 1 полностью заменяет мьютекс. Семафор не знает владельца, и release() из чужого потока пройдёт молча, а RLock в той же ситуации бросит RuntimeError.
Берут threading.Lock внутри асинхронного кода. Блокирующий захват останавливает весь event loop, нужен asyncio.Lock.
Считают, что при GIL блокировки не нужны, и получают плавающие ошибки в счётчиках и кэшах.