Как стать автором
Обновить

Многопоточный Python на примерах: токены отмены

Уровень сложностиСложный
Время на прочтение10 мин
Количество просмотров10K
Всего голосов 59: ↑54 и ↓5+49
Комментарии10

Комментарии 10

Я не понял, а где timeout в requests в завершающем примере. Нигде? Тогда: If no timeout is specified explicitly, requests do not time out. И уже не важно какой там токен. Сидим ждём. Ну и пока сидим и ждём, переписываем с реализацией настоящего прекращения по timeout.

После того, как перепишем, проверяем реализацию. Поскольку параметр timeout в requests не учитывает время после начала передачи данных, и есть риск опять сидеть и ждать.

Мне понравилось это замечание, таймаут на запрос как-то упустил в процессе подготовки статьи. Поправил пример кода.

Правильно я понимаю, что с multiprocessing работать не будет?

Верно.

Видимо, если вы в другой процесс передадите токен, то его не удастся отменить, но по таймауту-то отмена сработает?

Он не сериализуется для передачи другому процессу. Но даже если бы да, я бы не рекомендовал это использовать, инструмент предназначен только для многопоточки.

Вообще, я подумываю над тем, чтобы добавить поддержку процессов. Хотя это с высокой вероятностью придется выносить в отдельный инструмент.

Было бы удобно завести какой-нибудь токен для asyncio и его друзей.

Например, создаем токен и делаем await на него, после cancel он возвращает управление. Сейчас для таких целей использую asyncio.Event, но ваши токены выглядят удобнее.

В первую очередь хочется избавиться от конструкций вроде таких:

await asyncio.wait([e.wait() for e in stop_events], timeout=10, return_when=asyncio.FIRST_COMPLETED)

Да, это хорошая идея, я добавил такую возможность для всех токенов, можете попробовать.

Спасибо, но у меня есть несколько замечаний.

Во-первых, мне кажется, что введение метода wait только для асинхронных операций - не очень красиво. И не очень очевидно. Я бы предложил реализовать эту фичу и для синхронного режима тоже. Варианты: методы wait/wait_async, что-то вроде аналога asgiref.sync.sync_to_async или вообще декоратор.

Кстати, вместо `await token.wait()` может быть сделать просто `await token` ?

Во-вторых, ваша реализация с помощью while/sleep(0.0001) мне кажется очень неправильной. Смысл asyncio в том чтобы освобождать процессор, а вы заставляете его переключаться в вашу проверку и заново проверять все условия (втч синхронные) - каждый цикл эвентлупа. Простите, но уж лучше я останусь со своим вариантом. Должен сказать, что не могу сразу предложить вариант лучше. Возможно имеет смысл ввести отдельный асинхронный токен. И, возможно, не разрешать его смешивать с синхронными (по крайней мере поначалу). Поскольку, например ваша run_function теоретически тоже может пригодиться быть асинхронной.

Кстати, ваш CounterToken в этом методе лучше вообще принудительно выключать.

Замечание по документации, на которую вы дали ссылку - вы же в курсе что у вас код синхронный в примере? Несмотря на await, все строки кода будут выполняться последовательно. Метод asyncio.create_task подошел бы лучше для вызова do_something.

Безо всей этой асинхронной ерунды ваша реализация выглядит красиво. Не рассматривайте мои предложения как требования - может быть и оставить всё это синхронным..

Тут много хороших идей, часть из них я реализовал, а часть не буду.

Метод wait начиная с версии 0.0.11 является универсальным. По умолчанию он полностью синхронный, но если туда передать опциональным аргументом is_async=True, то метод можно эвейтить. await token выглядит красиво, но пока не реализовано. Возможно, когда-нибудь в будущем это случится, и я готов принимать PRы.

По поводу sleep. В действительности все проверки не прогоняются каждый цикл ивентлупа. Они прогоняются раз в промежуток времени, указанный пользователем как step. По умолчанию это действительно 0.0001 секунды в текущей реализации, однако его можно изменить, если кажется нужным. Чем реже проверки будут происходить - тем "дешевле" это будет для процессора, но тем сильнее можно "переспать" момент отмены.

Само по себе ожидание через sleep или что-то похожее (в нашем случае для асинхронного варианта используется asyncio.sleep), к сожалению, неизбежно, если мы хотим сделать универсальную библиотеку, а не заточенную конкретно под async. Если пойти по пути полной оптимизации под async, то для обычного питона она, к сожалению, станет работать хуже, то есть либо медленнее, либо с более распухшим API, дублирующим основные функции. Дело в том, что существует довольно много причин, по которым токен может быть отменен. Это может быть выполнение произвольного условия, вызов у токена метода cancel(), а также наступление любого из этих событий в любом из вложенных токенов. Некоторые кейсы, например вызов cancel(), действительно можно было бы более эффективно с асинхронной точки зрения обработать, засунув внутрь метода, скажем, вызов коллбека, который отменит какой-то из ожидабельных примитивов asyncio. Однако в этом случае библиотека для обычного, не асинхронного кода, начнет работать медленнее, а именно обычному коду я предпочитаю отдавать приоритет. Я практически не вижу способов сделать это красиво, не отъедая перфоманс у обычного (не async) варианта использования и не переусложняя код библиотеки.

По поводу автоотмены CounterToken, в нем уже по умолчанию была защита от скручивания теми запросами, которые он делает себе сам. Вот пример кода:

from cantok import CounterToken, TimeoutToken

counter_token = CounterToken(5)
timeout_token = TimeoutToken(5)

(counter_token + timeout_token).wait()

print(repr(counter_token))
# CounterToken(5, direct=True) - счетчик не скручен.

В статье я посчитал такие подробности излишними.

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий