Программирование SwiftКонкурентностьИнженер по разработке приложений на Swift

Гарантирует ли взаимный вызов двух actors через await взаимную блокировку?

Гарантирует ли взаимный вызов двух actors через await взаимную блокировку?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Нет, сам по себе взаимный вызов 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, могут произойти другие изменения состояния.

Минимальная иллюстрация:

actor A { let b: B func start() async { await b.respond(to: self) } func finish() { } } actor B { func respond(to a: A) async { await a.finish() } }

После вызова start actor A ожидает B, но не удерживает его исполнитель заблокированным. B вызывает finish у A, и A может обработать этот вызов, пока start приостановлен.

Это не означает, что циклические вызовы безопасны по смыслу. Например, если задача A ждёт завершения задачи B, B ждёт задачу A, а ни одна из них не может завершиться без другой, программа будет ждать бесконечно. Actor-изоляция защищает доступ к данным, но не устраняет циклы зависимостей между задачами.

Практическое правило: не полагайтесь на сохранение состояния actor через await. Перед ожиданием фиксируйте промежуточное состояние, а после возобновления проверяйте актуальность данных или заново принимайте решение.

Ситуация из практики

Есть CacheActor и NetworkActor. Кэш при промахе вызывает сеть, а сеть после ответа вызывает кэш для записи результата. Такой обратный вызов не обязан привести к deadlock, но усложняет порядок переходов, обработку ошибок и отмену.

Вариант с прямыми взаимными вызовами прост по структуре, но создаёт скрытую связанность и риск логических циклов. Вариант с общей блокировкой хуже согласуется с async-кодом: блокировка может удерживаться во время ожидания и действительно привести к deadlock или блокированию потока.

Предпочтительное решение — вернуть результат сетевого запроса в координатор, а затем отдельным вызовом записать его в кэш. Так, каждый actor отвечает за собственное состояние, между ними нет цикла ожидания, а отмену и ошибки можно обработать в одном месте. Результат — сохранение безопасности данных без скрытой взаимной зависимости.

Что кандидаты часто упускают

  1. Освобождает ли await actor навсегда до завершения метода?

Нет. await освобождает исполнитель actor только на время приостановки. После готовности результата продолжение метода снова выполняется с соблюдением actor-изоляции, но это уже не обязательно тот же непрерывный эксклюзивный фрагмент, который был до await.

  1. Можно ли считать последовательность действий actor транзакцией, если между ними есть await?

Нет. Между частями метода могут выполниться другие задачи и изменить состояние actor. Если операция требует целостности нескольких шагов, её нужно проектировать как явную транзакцию без промежуточного ожидания либо использовать проверку версии, резервирование состояния или повторную валидацию после await.

  1. Как предотвратить логический цикл ожидания между actors?

Нужно направить зависимости в одну сторону или вынести координацию в отдельный компонент. Часто помогает передача данных вместо передачи actor для обратного вызова: первый actor получает результат, завершает свою операцию, а координатор затем отдельно вызывает второй actor. Это снижает связанность и делает завершение, отмену и обработку ошибок явными.