Як правильно створювати та запускати паралельні завдання за допомогою модуля asyncio python?


76

Я намагаюся правильно зрозуміти та реалізувати два одночасно запущені Taskоб'єкти, використовуючи відносно новий asyncioмодуль Python 3 .

У двох словах, asyncio, здається, призначений для обробки асинхронних процесів та одночасного Taskвиконання через цикл подій. Він сприяє використанню await(застосовується в асинхронних функціях) як способу зворотного виклику для очікування та використання результату, не блокуючи цикл подій. (Ф'ючерси та зворотні виклики все ще є життєздатною альтернативою.)

Він також надає asyncio.Task()клас, спеціалізований підклас, Future призначений для обгортання програм. Переважно викликати за допомогою asyncio.ensure_future()методу. Призначене використання завдань asyncio - дозволити незалежно запущеним завданням виконуватись "одночасно" з іншими завданнями в тому ж циклі події. Я розумію, що Tasksвони підключені до циклу подій, який потім автоматично продовжує керувати спільною програмою між awaitоператорами.

Мені подобається ідея можливості використовувати одночасні Завдання без необхідності користуватися одним із Executorкласів, але я не знайшов великих деталей щодо реалізації.

Ось як я зараз це роблю:

import asyncio

print('running async test')

async def say_boo():
    i = 0
    while True:
        await asyncio.sleep(0)
        print('...boo {0}'.format(i))
        i += 1

async def say_baa():
    i = 0
    while True:
        await asyncio.sleep(0)
        print('...baa {0}'.format(i))
        i += 1

# wrap in Task object
# -> automatically attaches to event loop and executes
boo = asyncio.ensure_future(say_boo())
baa = asyncio.ensure_future(say_baa())

loop = asyncio.get_event_loop()
loop.run_forever()

У разі спроби одночасно запустити дві циклічні Завдання, я помітив, що якщо у Завдання немає внутрішнього awaitвиразу, воно застряє у whileциклі, фактично блокуючи інші завдання (майже як звичайний whileцикл). Однак, як тільки Завдання повинні (а) зачекати, вони, схоже, працюють одночасно без проблем.

Таким чином, awaitвисловлювання, здається, забезпечують цикл подій опорою для перемикання між завданнями вперед-назад, надаючи ефект паралельності.

Приклад виводу з внутрішнім await:

running async test
...boo 0
...baa 0
...boo 1
...baa 1
...boo 2
...baa 2

Приклад виводу без внутрішнього await:

...boo 0
...boo 1
...boo 2
...boo 3
...boo 4

Питання

Чи реалізується ця реалізація для `` належного '' прикладу одночасних циклічних завдань у asyncio?

Чи правильно, що єдиний спосіб, яким це працює, - це Taskнадати точку блокування ( awaitвираз) для того, щоб цикл подій жонглював кількома завданнями?


4
Так, завдання атомарно виконується від yield fromнаступного yield from.
Андрій Свєтлов

Відповіді:


81

Так, будь-яка підпрограма, яка працює у вашому циклі подій, заблокує запуск інших підпрограм і завдань, якщо тільки це не відбудеться

  1. Викликає іншу корутину за допомогою yield fromабо await(якщо використовується Python 3.5+).
  2. Повернення.

Це тому, що asyncioє однопоточним; єдиний спосіб запуску циклу подій полягає в тому, щоб жодна інша програма не виконувалась активно. Тимчасове використання yield from/ awaitпризупинення програми, що дає можливість циклу подій спрацювати.

Ваш прикладний код чудовий, але у багатьох випадках ви, мабуть, не хотіли б для початку тривалого коду, який не виконує асинхронний ввід / вивід, що працює в циклі подій. У цих випадках часто має сенс використовувати asyncio.loop.run_in_executorдля запуску код у фоновому потоці або процесі. ProcessPoolExecutorбуло б кращим вибором, якщо ваше завдання пов’язане з процесором, ThreadPoolExecutorбуло б використано, якщо вам потрібно виконати деякі введення-виведення, що не є asyncioдружнім.

Наприклад, ваші два цикли повністю пов'язані з процесором і не мають жодного стану, тому найкраща продуктивність буде забезпечена використанням ProcessPoolExecutorкожного циклу паралельно на центральних процесорах:

import asyncio
from concurrent.futures import ProcessPoolExecutor

print('running async test')

def say_boo():
    i = 0
    while True:
        print('...boo {0}'.format(i))
        i += 1


def say_baa():
    i = 0
    while True:
        print('...baa {0}'.format(i))
        i += 1

if __name__ == "__main__":
    executor = ProcessPoolExecutor(2)
    loop = asyncio.get_event_loop()
    boo = asyncio.create_task(loop.run_in_executor(executor, say_boo))
    baa = asyncio.create_task(loop.run_in_executor(executor, say_baa))

    loop.run_forever()

Дякую. Чудовий час, оскільки я просто цікавився саме цією темою щодо використання виконавців.
songololo

Спробувавши наведений вище код, я виявляю, що boo Task блокує роботу baa, якщо я не додаю вихід з asyncio.sleep (0) у кожну з циклів while True?
songololo

Також переробив біти run_in_executor наступним чином: loop.run_in_executor (виконавець, asyncio.Task (say_boo ()))
songololo

2
@shongololo Вибачте, виправлено. asyncio.asyncслід використовувати замість asyncio.Taskконструктора безпосередньо. Ми не хочемо say_booі не say_baaповинні бути спільними програмами, вони повинні бути просто звичайними функціями, які працюють поза циклом подій, тому вам не слід додавати yield fromдо них виклики чи обертати їх у asyncio.Task.
дано

1
Схоже, asyncio.async є псевдонімом, щоб забезпечити_футбуру, і зараз він застарів
srobinson

14

Вам не обов'язково потрібно, yield from xщоб надати контроль циклу подій.

У вашому прикладі, я думаю, правильним способом буде зробити yield Noneабо рівнозначно простий yield, а не yield from asyncio.sleep(0.001):

import asyncio

@asyncio.coroutine
def say_boo():
  i = 0
  while True:
    yield None
    print("...boo {0}".format(i))
    i += 1

@asyncio.coroutine
def say_baa():
  i = 0
  while True:
    yield
    print("...baa {0}".format(i))
    i += 1

boo_task = asyncio.async(say_boo())
baa_task = asyncio.async(say_baa())

loop = asyncio.get_event_loop()
loop.run_forever()

Програми - це звичайні старі генератори Python. Внутрішньо asyncioцикл подій веде запис цих генераторів і викликає gen.send()кожного з них по одному в нескінченний цикл. Кожного разу, коли ви yield, дзвінок gen.send()завершується і цикл може рухатися далі. (Я спрощую це; огляньте https://hg.python.org/cpython/file/3.4/Lib/asyncio/tasks.py#l265 для фактичного коду)

Тим не менш, я все одно піду по run_in_executorшляху, якщо вам потрібно робити інтенсивні обчислення процесора без обміну даними.


Працює в Python 3.4, але, здається, не працює в Python 3.5. Чи існує подібний підхід для 3.5? ( Noneздається, більш елегантний, ніж використання asyncio.sleep()скрізь ...)
songololo

22
Починаючи з Python 3.5, правильний спосіб зробити це за допомогою asyncio.sleep(0). Дивіться це обговорення.
Jashandeep Sohi
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.