Перейти к содержимому
НейроКуб
Открытый материал · 3 мин
Даниил Баланс

В Ray есть async, а ping всё равно ждёт: проверьте, кто блокирует цикл

Почему max_concurrency не спасает от блокирующего кода внутри AsyncActor. Учебный пример и критерий выбора между асинхронным ожиданием, потоками и отдельными задачами.

AsyncActor в Ray может принимать несколько вызовов, но один блокирующий метод способен задержать остальные. Для сервиса вокруг ИИ-модели это означает: запрос проверки состояния ждёт вместе с долгой обработкой, хотя лимит конкурентности ещё не исчерпан.

Где заканчивается конкурентность

В документации Ray описан один цикл событий внутри асинхронного актора. Методы делят его и передают управление при подходящем await. Параметр max_concurrency ограничивает число одновременно обслуживаемых вызовов, но не превращает их в отдельные вычислительные потоки.

Поэтому вызовы ray.get и ray.wait внутри async-метода актора запрещены: они блокируют цикл. Добавление async def к обычной функции тоже не делает её содержимое неблокирующим. Если библиотека долго считает или синхронно ждёт сеть, соседний метод не получает возможности продолжить работу.

Проверка на маленьком примере

Представьте актора, который запрашивает внешний сервис распознавания и отдельно отвечает на ping. В учебной заготовке ниже ожидание сервиса заменено таймером. Это демонстрация организации ожидания, а не тест скорости реальной модели.

import asyncio
import ray

@ray.remote
class Probe:
    async def infer(self):
        await asyncio.sleep(2)
        return "done"

    async def ping(self):
        return "alive"

p = Probe.options(max_concurrency=2).remote()
slow = p.infer.remote()
print(ray.get(p.ping.remote()))
ray.get(slow)

Запускайте пример в отдельной тестовой среде с установленным Ray. Внешний ray.get здесь выполняется в вызывающем процессе, а не внутри актора. Для проверки своего обработчика добавьте отметки начала и окончания обоих методов: важно увидеть, обслуживается ли ping во время реального ожидания. Порядок отправки сам по себе ещё не доказывает порядок исполнения.

У asyncio.sleep ожидание передаёт управление другим задачам. Синхронный time.sleep внутри того же async-метода такого эффекта не даёт. Сначала сравните эти два варианта, затем подключайте сетевой клиент: так проще отделить ошибку организации кода от задержки внешнего сервиса.

Что выбрать для рабочего сервиса

Если операция поддерживает асинхронное ожидание, используйте соответствующий API. Для блокирующего ввода-вывода Python предлагает asyncio.to_thread; при этом отдельно проверьте безопасность совместного доступа к клиенту и состоянию. Из-за GIL этот приём обычно не превращает тяжёлый Python-расчёт в параллельный.

Сравнение проводите при одинаковом числе запросов и одинаковой работе обработчика. Отдельно запишите время ответа ping и завершения длинного вызова. Если меняете сразу клиента, ресурсы и число акторов, по результату уже нельзя понять, помогло ли именно освобождение цикла событий. Проверка на таймере лишь задаёт контрольный сценарий.

Для вычислений рассмотрите отдельные задачи или процессы; для потокового актора Ray нужна другая модель исполнения. Наличие хотя бы одного async def заставляет Ray распознать класс как AsyncActor. Практический критерий выбора — не слово async в объявлении, а способность короткого служебного вызова завершиться, пока длинный запрос ещё выполняется.

Комментарии

Пока нет комментариев. Начните обсуждение.