web-dev-qa-db-ja.com

Pythonのasyncioモジュールを使用して並行タスクを適切に作成および実行する方法は?

同時に実行される2つの Task オブジェクトをPython 3の比較的新しい asyncio モジュールを使用して適切に理解および実装しようとしています。

一言で言えば、asyncioは非同期プロセスと、イベントループでのTaskの同時実行を処理するように設計されているようです。イベントループをブロックせずに、結果を待機して使用するコールバックフリーの方法として、await(非同期関数に適用)の使用を促進します。 (未来とコールバックはまだ実行可能な選択肢です。)

また、コルーチンをラップするように設計されたFutureの特殊なサブクラスであるasyncio.Task()クラスも提供します。 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()

2つのループタスクを同時に実行しようとすると、タスクに内部await式がない限り、タスクがwhileループでスタックし、他のタスクの実行を事実上ブロックします(通常のwhileループのように)。ただし、タスクは(a)待機する必要があるとすぐに、問題なく並行して実行されるようです。

したがって、awaitステートメントは、タスク間で前後に切り替えるための足場をイベントループに提供し、同時実行の効果を提供しているように見えます。

内部awaitを使用した出力例:

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

出力例withoutinternal await

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

ご質問

この実装は、asyncioの同時ループタスクの「適切な」例に合格していますか?

これが機能する唯一の方法は、Taskがブロッキングポイント(await式)を提供して、イベントループが複数のタスクをジャグリングすることであるということは正しいですか?

63
songololo

はい。イベントループ内で実行されているコルーチンは、他のコルーチンとタスクの実行をブロックします。

  1. yield fromまたはawaitを使用して別のコルーチンを呼び出します(Python 3.5+を使用している場合)。
  2. 返却値。

これは、asyncioがシングルスレッドであるためです。イベントループを実行する唯一の方法は、他のコルーチンがアクティブに実行されないようにすることです。 yield from/awaitを使用すると、コルーチンが一時的に中断され、イベントループが機能する機会が与えられます。

サンプルコードは問題ありませんが、多くの場合、最初からイベントループ内で非同期I/Oを実行していない長時間実行されるコードは望ましくないでしょう。このような場合、 BaseEventLoop.run_in_executor を使用してバックグラウンドスレッドまたはプロセスでコードを実行する方が適切な場合がよくあります。タスクがCPUバウンドの場合はProcessPoolExecutorの方が適しています。ThreadPoolExecutorに対応していないI/Oを行う必要がある場合は、asyncioが使用されます。

たとえば、2つのループは完全にCPUバウンドであり、状態を共有しないため、ProcessPoolExecutorを使用してCPU間で各ループを並列に実行すると、最高のパフォーマンスが得られます。

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 __== "__main__":
    executor = ProcessPoolExecutor(2)
    loop = asyncio.get_event_loop()
    boo = asyncio.ensure_future(loop.run_in_executor(executor, say_boo))
    baa = asyncio.ensure_future(loop.run_in_executor(executor, say_baa))

    loop.run_forever()
73
dano

イベントループを制御するためにyield from xは必ずしも必要ではありません。

あなたの例では、proper方法は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()を1つずつ呼び出します。 yieldを呼び出すたびに、gen.send()の呼び出しが完了し、ループを続行できます。 (私はそれを単純化しています; https://hg.python.org/cpython/file/3.4/Lib/asyncio/tasks.py#l265 を実際のコードについて見てみましょう)

ただし、データを共有せずにCPUを集中的に使用する計算を行う必要がある場合は、run_in_executorルートを使用します。

13
Jashandeep Sohi