Гарантирует ли взаимный вызов двух actors через await взаимную блокировку?
Нет, сам по себе взаимный вызов actors через await не гарантирует deadlock. При приостановке actor освобождает свой исполнитель, поэтому он может обработать другой изолированный вызов, включая обратный вызов. Однако циклическая зависимость задач или ожидание результата, который логически никогда не будет произведён, всё равно может привести к бесконечному ожиданию.
Actors появились как модель изоляции изменяемого состояния без ручного управления блокировками. В традиционной многопоточности взаимный вызов под удерживаемыми mutex часто создаёт классический deadlock: поток A удерживает блокировку A и ждёт B, а поток B удерживает B и ждёт A.
В Swift actor-изоляция сочетается с кооперативной асинхронностью. Это позволяет не удерживать эксклюзивный доступ к actor на протяжении всего времени ожидания внешней операции.
Метод actor может обратиться к другому actor и приостановиться на await. Если считать actor обычной блокировкой, легко сделать неверный вывод, что обратный вызов обязательно остановится навсегда.
Реальный риск другой: после await состояние первого actor может быть изменено другой задачей. Кроме того, две задачи могут образовать цикл ожидания результатов, который не блокирует исполнители технически, но никогда не завершается.
До await actor выполняет изолированный участок последовательно с другими обращениями к своему состоянию. На точке приостановки текущая задача перестаёт занимать исполнитель actor, и actor может начать выполнять другой готовый изолированный вызов.
Поэтому обратный вызов к исходному actor обычно может выполниться до продолжения первого метода. Это называется реентерабельностью actor: между двумя частями одного метода, разделёнными await, могут произойти другие изменения состояния.
Минимальная иллюстрация:
После вызова start actor A ожидает B, но не удерживает его исполнитель заблокированным. B вызывает finish у A, и A может обработать этот вызов, пока start приостановлен.
Это не означает, что циклические вызовы безопасны по смыслу. Например, если задача A ждёт завершения задачи B, B ждёт задачу A, а ни одна из них не может завершиться без другой, программа будет ждать бесконечно. Actor-изоляция защищает доступ к данным, но не устраняет циклы зависимостей между задачами.
Практическое правило: не полагайтесь на сохранение состояния actor через await. Перед ожиданием фиксируйте промежуточное состояние, а после возобновления проверяйте актуальность данных или заново принимайте решение.
Есть CacheActor и NetworkActor. Кэш при промахе вызывает сеть, а сеть после ответа вызывает кэш для записи результата. Такой обратный вызов не обязан привести к deadlock, но усложняет порядок переходов, обработку ошибок и отмену.
Вариант с прямыми взаимными вызовами прост по структуре, но создаёт скрытую связанность и риск логических циклов. Вариант с общей блокировкой хуже согласуется с async-кодом: блокировка может удерживаться во время ожидания и действительно привести к deadlock или блокированию потока.
Предпочтительное решение — вернуть результат сетевого запроса в координатор, а затем отдельным вызовом записать его в кэш. Так, каждый actor отвечает за собственное состояние, между ними нет цикла ожидания, а отмену и ошибки можно обработать в одном месте. Результат — сохранение безопасности данных без скрытой взаимной зависимости.
await actor навсегда до завершения метода?Нет. await освобождает исполнитель actor только на время приостановки. После готовности результата продолжение метода снова выполняется с соблюдением actor-изоляции, но это уже не обязательно тот же непрерывный эксклюзивный фрагмент, который был до await.
await?Нет. Между частями метода могут выполниться другие задачи и изменить состояние actor. Если операция требует целостности нескольких шагов, её нужно проектировать как явную транзакцию без промежуточного ожидания либо использовать проверку версии, резервирование состояния или повторную валидацию после await.
Нужно направить зависимости в одну сторону или вынести координацию в отдельный компонент. Часто помогает передача данных вместо передачи actor для обратного вызова: первый actor получает результат, завершает свою операцию, а координатор затем отдельно вызывает второй actor. Это снижает связанность и делает завершение, отмену и обработку ошибок явными.