Python asyncio: войти во временный асин c контекст? - PullRequest
1 голос
/ 31 марта 2020

Я хочу написать библиотеку, которая смешивает синхронную и асинхронную работу, например:

def do_things():
    # 1) do sync things
    # 2) launch a bunch of slow async tasks and block until they are all complete or an exception is thrown
    # 3) more sync work
    # ...

Я начал реализовывать это, используя asyncio в качестве предлога для изучения библиотеки, но по мере того, как я узнаю больше кажется, что это может быть неправильный подход. Моя проблема в том, что, похоже, нет чистого способа сделать 2, потому что это зависит от контекста вызывающей стороны. Например:

  • Я не могу использовать asyncio.run(), поскольку у вызывающего абонента уже может быть запущено событие l oop, а у вас может быть только один l oop на поток.
  • Маркировка do_things как async слишком тяжелая, поскольку не требует , чтобы вызывающий абонент был асин c. Кроме того, если do_things было async, вызов синхронного кода (1 & 3) из функции async представляется плохой практикой .
  • asyncio.get_event_loop() также кажется неправильным, потому что это может создать новый l oop, который, если оставить его запущенным, помешает вызывающей стороне создать свой собственный l oop после вызова do_things (хотя возможно, они не должны этого делать). И на основании документации loop.close похоже, что запуск / остановка нескольких циклов в одном потоке не сработает.

По сути, кажется, что если я хочу использовать asyncio вообще, я вынужден использовать его на протяжении всего времени жизни программы, и поэтому все библиотеки, подобные этой, должны быть записаны как на 100% синхронные или на 100% асинхронные. Но мне нужно следующее: используйте текущее событие l oop, если оно запущено, в противном случае создайте временное событие только для области действия 2 и не нарушайте при этом клиентский код. Существует ли что-то подобное или asyncio неправильный выбор?

1 Ответ

3 голосов
/ 31 марта 2020

Я не могу использовать asyncio.run (), потому что у вызывающей стороны уже может быть запущено событие l oop, и вы можете иметь только один l oop на поток.

Если у вызывающего абонента есть запущенное событие l oop, вы не должны запускать блокирующий код в первую очередь, потому что это заблокирует вызывающего абонента l oop!

С учетом этого, ваш лучший вариант действительно должен do_things asyn c и вызывать код syn c с использованием run_in_executor, который предназначен именно для этого случая использования:

async def do_things():
    loop = asyncio.get_event_loop()
    await loop.run_in_executor(None, sync_stuff)
    await async_func()
    await loop.run_in_executor(None, more_sync_stuff)

Эта версия do_things может использоваться из asyn c код как await do_things() и из syn c код как asyncio.run(do_things()).

Сказав это ... если вы знаете, что код syn c будет работать очень быстро, или вы по какой-то причине, желая заблокировать событие вызывающего абонента l oop, вы можете обойти ограничение, запустив событие l oop в отдельном потоке:

def run_async(aw):
    result = None
    async def run_and_store_result():
        nonlocal result
        result = await aw
    t = threading.Thread(target=asyncio.run, args=(run_and_store_result(),))
    t.start()
    t.join()
    return result

do_things может выглядеть так :

async def do_things():
    sync_stuff()
    run_async(async_func())
    more_sync_stuff()

Будет вызываться из кода syn c и асин c, но стоимость будет такой: * 10 24 *

  • это создаст совершенно новое событие l oop каждый раз. (Хотя вы можете кэшировать событие l oop и никогда не выходить из него.)
  • при вызове из асинхронного кода c, оно заблокирует событие вызывающего абонента l oop, таким образом, эффективно нарушая его асинхронное использование, даже если большая часть времени фактически проводится внутри собственного асин c кода.
...