В чем разница между обычным lock и рекурсивным lock и почему использовать обычный
Что проверяют
Понимаете ли вы, что рекурсивный lock не «более удобная версия» обычного, а способ спрятать ошибку в дизайне. Следом обычно спрашивают, что будет при повторном захвате NSLock и os_unfair_lock в одном потоке и чем вы синхронизируете состояние в современном Swift.
Как отвечать
- обычный lock (
NSLock,os_unfair_lock,pthread_mutex_tпо умолчанию): повторный захват тем же потоком не разрешён.NSLockпри этом повиснет навсегда, аos_unfair_lockотслеживает владельца и роняет приложение. Отпускать lock должен тот же поток, что его взял; - рекурсивный lock (
NSRecursiveLock,@synchronizedв Objective-C, мьютекс сPTHREAD_MUTEX_RECURSIVE): помнит владельца и счётчик захватов, освобождается, когда счётчик вернулся к нулю; - почему обычный: lock защищает инвариант. Внутри критической секции инвариант бывает временно нарушен, и повторный вход из того же потока (через делегат, замыкание, KVO) увидит состояние наполовину изменённым. Рекурсивный lock такой вход молча пропускает, обычный сразу показывает проблему. К тому же он дешевле: не нужно вести владельца и счётчик.
Повторный захват убирают так: публичный метод берёт lock и вызывает приватный, который работает в предположении, что lock уже взят. Колбэки наружу вызывают после выхода из секции.
Из современных средств назовите OSAllocatedUnfairLock (iOS 16) с методом withLock, Mutex из фреймворка Synchronization (iOS 18) и акторы, если код уже на Swift Concurrency.
Где спотыкаются
Берут NSRecursiveLock «на всякий случай», чтобы не ловить дедлоки. Дедлок пропадает, а гонка по инварианту остаётся и проявляется реже и страшнее.
Держат lock через await. Компилятор предупреждает о вызове lock() из асинхронного контекста не зря: после await задача может продолжиться на другом потоке, и отпускать lock будет не тот поток, который его взял.
Используют рекурсивный мьютекс вместе с условной переменной. Ожидание может снять только один уровень захвата, мьютекс остаётся занятым, и поток, который должен подать сигнал, в секцию не попадёт.