Понимание AsyncIO и порядка потоков на простом примере - PullRequest
0 голосов
/ 26 февраля 2020

У меня есть этот код:

import asyncio

async def part1():
    print('1')
    await asyncio.sleep(2)
    print('5')


async def part2():
    print('2')
    await asyncio.sleep(2)
    print('6')


async def main():
    p1 = await part1()
    p2 = await part2()
    print('3')
    print('4')

asyncio.run(main())

Когда я запусту его, я бы ожидал напечатать

1
2
3
4
5
6

Как я понимаю, поток должен go выглядеть так:

  1. Запускает функцию main ()
  2. Запускает функцию part1 ()
  3. Печатает 1
  4. В течение 2 секунд ожидания она должна продолжаться на line p2 = await part2 ()
  5. Запускает функцию part2 ()
  6. Печатает 2
  7. Пока он ждет 2 секунды, он должен продолжиться по линии main () " print ('3') ", затем" print ('4') "
  8. part1 () завершает свой сон, поэтому она печатает 5
  9. part2 (), функция завершает свой сон, поэтому он печатает 6

Тем не менее, он печатает:

1
5
2
6
3
4

и ждет обоих asyn c .sleep (2)

Что мне здесь не хватает?

Спасибо!

Ответы [ 2 ]

0 голосов
/ 27 февраля 2020

В ответ на ваш комментарий:

... [T] Я понимаю, что когда я звоню main() через asyncio.run(), это создает своего рода «обертку» или объект, который отслеживает поток и пытается выполнить вычисления, когда асинхронная функция простаивает

(Небольшой отказ от ответственности - почти весь мой опыт с асинхронными c материалами находится в C#, но ключевые слова await в каждом из них выглядят довольно хорошо по поведению)

Ваше понимание здесь более или менее правильно - asyncio.run(main()) запустит main() в отдельной (фоновой) "теме".

( Примечание: здесь я игнорирую специфику GPL и однопоточности python. Для пояснения достаточно «отдельного потока». )

Недоразумение возникает из-за того, как вы думаете, await работает, как оно на самом деле работает и как вы организовали свой код. Я действительно не смог найти достаточного описания того, как Python await работает, кроме как в PEP , который представил его:

await, аналогично yield из приостанавливает выполнение сопрограммы read_data до тех пор, пока ожидаемое завершение db.fetch не вернет данные результата.

С другой стороны, C# await имеет намного больше документация / объяснение , связанное с ним:

Когда применяется ключевое слово await, оно приостанавливает вызывающий метод и возвращает управление вызывающей стороне до тех пор, пока ожидаемая задача не будет завершено.

В вашем случае «средний выход» (3 и 4) предшествовал 2 ожиданиям, оба из которых вернут управление на asycnio.run(...), пока не получат результат.

Вот код, который я использовал и который дает мне результат, который вы ищете:

import asyncio

async def part1():
    print('1')
    await asyncio.sleep(2)
    print('5')


async def part2():
    print('2')
    await asyncio.sleep(2)
    print('6')


async def part3():
    print('3')
    print('4')

async def main():
    t1 = asyncio.create_task(part1())
    t2 = asyncio.create_task(part2())
    t3 = asyncio.create_task(part3())
    await t1
    await t2
    await t3

asyncio.run(main())

Вы заметите, что я превратил ваш main в мой part3 и создал новый main. В новом main я создаю отдельный ожидаемый Task для каждой части (1, 2 и 3). Затем я await последовательно.

Когда t1 выполняется, он достигает await после первого отпечатка. Это делает паузу part1 в этой точке, пока ожидаемое не завершится. Управление программой вернется к вызывающей стороне (main) до этой точки, подобно тому, как работает yield.

Пока t1 находится в режиме "паузы" (ожидания), main будет продолжаться и запускаться до t2. t2 делает то же самое, что и t1, поэтому вскоре после этого будет запущен t3. t3 не выполняет await -ing, поэтому его вывод происходит немедленно.

В этот момент main просто ожидает, пока его дочерний элемент Task s не завершится sh up. t1 был await -ed первым, поэтому он вернется первым, а затем t2. Конечный результат (где test.py - сценарий, в который я поместил это):

~/.../> py .\test.py
1
2
3
4
5
6
0 голосов
/ 27 февраля 2020
Пока он ждет 2 секунды, он должен продолжаться

Это недоразумение. await означает с точностью до наоборот, что он должен , а не продолжать (запуск этой конкретной сопрограммы), пока не будет достигнут результат. Это «wait» в «await».

Если вы хотите продолжить, вы можете использовать:

    # spawn part1 and part2 as background tasks
    t1 = asyncio.create_task(part1())
    t2 = asyncio.create_task(part2())
    # and now await them while they run in parallel
    p1 = await t1
    p2 = await t2

Более простой способ добиться того же эффекта - с помощью утилиты gather function:

    p1, p2 = asyncio.gather(part1(), part2())

Обратите внимание, что код, измененный таким образом, все равно не будет выводить 1 2 3 4 5 6, потому что окончательные распечатки не будут выполняться до тех пор, пока задачи не будут завершены sh. В результате фактический результат будет 1 2 5 6 3 4.

...