Какое ключевое последствие может возникнуть при удержании синхронной блокировки через await?
Удержание синхронной блокировки через await может привести к взаимной блокировке, голоданию задач или исчерпанию потоков исполнителя. При приостановке задачи Swift не освобождает автоматически NSLock, Mutex или другую блокировку: она остаётся захваченной до явного освобождения.
Синхронные блокировки появились для коротких критических секций, которые выполняются без приостановки: поток захватывает блокировку, быстро изменяет состояние и освобождает её. async/await добавляет возможность приостановить задачу, причём во время ожидания поток может быть отдан другим задачам.
Проблема возникает из-за смешения двух моделей: блокировка привязана к ресурсу и удерживается синхронно, а await разделяет выполнение задачи на участки, между которыми может пройти произвольное время.
Если задача захватила блокировку, а затем выполнила await, другая задача может попытаться получить ту же блокировку. Она не приостановится как async-операция, а заблокирует поток исполнителя на синхронном ожидании.
Это опасно по нескольким причинам:
await не является границей критической секции и не вызывает unlock. Более того, после возобновления задача может продолжить работу на другом потоке, поэтому рассчитывать на привязку блокировки к конкретному потоку нельзя.
В примере блокировка удерживается во время приостановки. Сам по себе Task.yield() не гарантирует deadlock, но в реальном коде ожидаемая операция может косвенно потребовать тот же ресурс, что создаст циклическое ожидание.
Безопасный принцип: не пересекать await с синхронной критической секцией. Нужно выполнить асинхронную часть до захвата блокировки, затем быстро изменить защищённое состояние и сразу освободить блокировку.
Если состояние естественно обслуживается асинхронно, обычно лучше использовать actor: его изолированные участки не блокируют поток при ожидании. Однако actor не делает составную последовательность атомарной через await; если между чтением и записью есть приостановка, состояние может измениться.
Для полностью синхронной короткой операции подходят Mutex или другая синхронная блокировка, но внутри такой критической секции также нельзя выполнять await. Компромисс состоит в том, что actor удобнее для асинхронного владения состоянием, а mutex может быть эффективнее для очень коротких синхронных операций.
Сервис кэширования получает данные по сети и обновляет общий словарь. Разработчик захватывает mutex перед сетевым запросом, чтобы «защитить весь сценарий», а затем выполняет await.
Вариант с удержанием mutex через сетевой await прост, но блокирует доступ к кэшу на всё время запроса и может исчерпать потоки. Вариант с полным отказом от синхронизации допускает гонки данных. Вариант с actor сериализует доступ к словарю, но требует учитывать повторное вхождение и возможное изменение состояния после каждого await.
Оптимальное решение — хранить кэш внутри actor, выполнять сетевой запрос без удержания синхронной блокировки, а затем снова обратиться к actor для проверки актуальности и записи результата. Это предотвращает блокирование потоков и позволяет явно обработать ситуацию, когда другой запрос уже обновил тот же ключ.
1. Освобождает ли await автоматически NSLock?
Нет. await может приостановить задачу и освободить поток, но состояние внешней синхронной блокировки не меняется. Блокировка будет удерживаться до вызова unlock, включая весь период ожидания.
2. Всегда ли удержание mutex через await приводит к deadlock?
Нет, гарантированный deadlock возникает только при наличии подходящего цикла ожиданий. Но даже без него код может блокировать потоки, ухудшать планирование и приводить к исчерпанию доступных потоков, поэтому такой шаблон всё равно считается небезопасным.
3. Можно ли заменить mutex на actor и сохранить ту же атомарность?
Не автоматически. Actor сериализует отдельные изолированные обращения, но после await его состояние может быть изменено другой задачей. Если операция состоит из нескольких шагов, нужно либо выполнять их без приостановки внутри actor, либо проектировать явную проверку версии, транзакционный метод или другой протокол согласования.