В actor выполняется долгий синхронный участок: что происходит с ожидающими изолированными вызовами?
Ожидающие вызовы того же actor не выполняются параллельно с долгим синхронным участком: они ждут, пока текущая работа завершится или приостановится на await. Actor гарантирует последовательный доступ к своему изолированному состоянию, но не гарантирует справедливое распределение времени или быстрый прогресс ожидающих задач.
Поэтому длительные вычисления без точек приостановки внутри actor могут создать задержки и фактически блокировать обслуживание его очереди.
Модель actors появилась как способ изолировать изменяемое состояние и заменить ручную синхронизацию сообщений между потоками. Вместо одновременного доступа к данным actor последовательно выполняет изолированные участки.
Эта модель решает проблему гонок данных, но не превращает любой код внутри actor в неблокирующий. Последовательность выполнения означает, что одна длительная работа может задержать остальные обращения к тому же состоянию.
Предположим, actor обрабатывает запрос, внутри которого выполняется большой цикл, синхронный парсинг или вычисление без await. Пока этот участок не завершён, другие задачи не могут выполнить его изолированные методы, даже если они делают короткую операцию чтения.
Последствиями могут стать рост задержки UI, тайм-ауты сетевых операций, накопление очереди сообщений и впечатление, что конкурентность «не работает». При этом задачи, не связанные с данным actor, обычно могут продолжать выполняться.
Изолированный метод actor исполняется как последовательная работа на executor этого actor. Пока работа не достигла точки приостановки или не вернула управление, следующая изолированная работа этого же actor не получает доступа к его состоянию.
await может приостановить текущую задачу и освободить actor для другой работы. Однако сам по себе вызов await не делает синхронное вычисление параллельным: если до await выполняется длинный CPU-цикл, actor остаётся занят.
Если process получает большой массив, вызов status для того же экземпляра будет ждать завершения вычисления. Практическое решение — вынести тяжёлую работу в отдельный компонент или задачу, а в actor оставить короткое чтение и изменение состояния. Результат следует возвращать в actor через безопасную границу конкурентности, не передавая несамопередаваемые данные без необходимой изоляции.
Разбиение вычисления на части может уменьшить задержки, но требует аккуратного проектирования: между частями нужно сохранять корректность состояния, а частые переключения увеличивают накладные расходы. Нельзя переносить произвольный доступ к состоянию actor во внешний код только ради производительности.
В экранном actor хранится кэш изображений. Метод загрузки после получения данных выполняет на actor дорогое декодирование большого изображения, поэтому одновременно поступивший запрос на проверку состояния экрана начинает ждать несколько сотен миллисекунд.
Вариант с выполнением декодирования прямо в actor прост и безопасен для состояния, но задерживает все остальные операции. Вариант с обычным общим изменяемым кэшем быстрее организовать, однако он создаёт риск гонок и требует отдельной синхронизации.
Выбранное решение — оставить в actor только состояние кэша и короткие операции его чтения и обновления, а декодирование выполнять вне actor с передаваемым Sendable результатом. После завершения вычисления actor повторно проверяет актуальность запроса и атомарно обновляет кэш. Это уменьшает время занятости actor, сохраняя безопасность данных.
Освобождает ли actor очередь сразу после начала async-метода?
Нет. Асинхронность метода не означает автоматического разделения его тела на независимые части. Actor освобождается только в точке фактической приостановки, например при ожидании незавершённой async-операции.
Код до первой такой точки выполняется как обычный синхронный участок и может задержать другие изолированные вызовы. Поэтому объявление метода как async само по себе не решает проблему долгих вычислений.
Можно ли считать actor средством планирования CPU-времени?
Нет. Actor предоставляет изоляцию состояния и последовательное исполнение изолированных работ, но не обещает квант времени, приоритетное обслуживание или отсутствие голодания конкретного вызова.
Если нужна контролируемая обработка тяжёлых операций, её следует проектировать отдельно: ограничивать объём работы в одном участке, использовать подходящий исполнитель или очередь и не смешивать защиту состояния с планированием вычислений.
Безопасно ли вынести вычисление из actor, а затем без проверки записать результат обратно?
Безопасность доступа к памяти ещё не гарантирует логическую актуальность результата. Пока вычисление выполнялось вне actor, состояние могло измениться или запрос мог стать устаревшим.
После возвращения результата в actor нужно проверить версию, идентификатор запроса или иное условие актуальности. Только затем результат следует применять к изолированному состоянию; иначе гонки данных не будет, но появится ошибка устаревшего обновления.