Python 3.5のコルーチンとフューチャー/タスクの違いは?


99

ダミー関数があるとしましょう:

async def foo(arg):
    result = await some_remote_call(arg)
    return result.upper()

違いは何ですか:

coros = []
for i in range(5):
    coros.append(foo(i))

loop = get_event_loop()
loop.run_until_complete(wait(coros))

そして:

from asyncio import ensure_future

futures = []
for i in range(5):
    futures.append(ensure_future(foo(i)))

loop = get_event_loop()
loop.run_until_complete(wait(futures))

:この例では結果が返されますが、これは質問の焦点では​​ありません。戻り値が重要な場合は、のgather()代わりに使用してくださいwait()

戻り値に関係なく、私は明快さを探していensure_future()ます。wait(coros)そして、wait(futures)コルーチンを実行し、そのとき、なぜコルーチンはに包まれるべき両方ensure_future

基本的に、Python 3.5を使用して一連の非ブロッキング操作を実行する正しい方法(tm)はasync何ですか?

追加のクレジットについて、コールをバッチ処理する場合はどうなりますか?たとえば、some_remote_call(...)1000回呼び出す必要がありますが、同時接続数が1000のWebサーバー/データベースなどを壊したくありません。これはスレッドまたはプロセスプールで実行できますが、これを行う方法はありasyncioますか?

回答:


94

コルーチンは、値を生成し、外部から値を受け入れることができるジェネレーター関数です。コルーチンを使用する利点は、関数の実行を一時停止し、後で再開できることです。ネットワーク操作の場合、応答を待っている間、関数の実行を一時停止することは理にかなっています。時間を使用して、他のいくつかの関数を実行できます。

未来はPromiseJavaScript のオブジェクトのようなものです。これは、将来実現する価値のプレースホルダーのようなものです。上記の場合、ネットワークI / Oを待機している間、関数はコンテナーを提供できます。操作が完了すると、コンテナーは値をコンテナーに埋め込むことができます。futureオブジェクトを保持し、それが実行されると、そのオブジェクトのメソッドを呼び出して実際の結果を取得できます。

直接回答:あなたは必要ありませんensure_futureあなたが結果を必要としない場合。結果が必要な場合や、発生した例外を取得する場合に適しています。

追加のクレジット:最大ワーカー数を制御run_in_executorするExecutorインスタンスを選択して渡します。

説明とサンプルコード

最初の例では、コルーチンを使用しています。このwait関数はコルーチンの束を取り、それらを結合します。したがってwait()、すべてのコルーチンが使い果たされたときに完了します(すべての値を返す完了/終了)。

loop = get_event_loop() # 
loop.run_until_complete(wait(coros))

このrun_until_completeメソッドは、実行が終了するまでループが生きていることを確認します。この場合、非同期実行の結果が得られないことに注意してください。

2番目の例では、ensure_future関数を使用してコルーチンをラップしTask、一種のオブジェクトを返しますFuture。コルーチンは、を呼び出すと、メインイベントループで実行されるようにスケジュールされますensure_future。返されたfuture / taskオブジェクトにはまだ値がありませんが、時間の経過とともにネットワーク操作が完了すると、futureオブジェクトは操作の結果を保持します。

from asyncio import ensure_future

futures = []
for i in range(5):
    futures.append(ensure_future(foo(i)))

loop = get_event_loop()
loop.run_until_complete(wait(futures))

したがって、この例では、単にコルーチンを使用する代わりにフューチャーを使用していることを除いて、同じことを行っています。

asyncio / coroutines / futuresの使用例を見てみましょう。

import asyncio


async def slow_operation():
    await asyncio.sleep(1)
    return 'Future is done!'


def got_result(future):
    print(future.result())

    # We have result, so let's stop
    loop.stop()


loop = asyncio.get_event_loop()
task = loop.create_task(slow_operation())
task.add_done_callback(got_result)

# We run forever
loop.run_forever()

ここではcreate_taskloopオブジェクトに対してメソッドを使用しました。ensure_futureメインイベントループでタスクをスケジュールします。この方法により、選択したループでコルーチンをスケジュールできます。

またadd_done_callback、タスクオブジェクトのメソッドを使用してコールバックを追加する概念も示しています。

A Taskdone、コルーチンが値を返すとき、例外を発生させるとき、またはキャンセルされるときです。これらのインシデントを確認する方法があります。

これらのトピックに関するブログ投稿をいくつか作成しました。

もちろん、公式マニュアルで詳細を見つけることができます:https : //docs.python.org/3/library/asyncio.html


3
質問をもう少し明確にするために更新しました。コルーチンからの結果が必要ない場合でも、使用する必要がありますensure_future()か?そして、結果が必要な場合は、単に使用できませんrun_until_complete(gather(coros))か?
kniteは

1
ensure_futureイベントループで実行されるコルーチンをスケジュールします。だから私はそう言うでしょう、それは必須です。もちろん、他の関数/メソッドを使用してコルーチンをスケジュールすることもできます。はい、使用できますgather()が、収集はすべての応答が収集されるまで待機します。
masnun

5
@AbuAshrafMasnun @knite gatherwait使用して、指定されたコルーチンをタスクとして実際にラップensure_futureします(ここここのソースを参照)。したがってensure_future、事前に使用する意味はなく、結果を取得するかどうかとは関係ありません。
Vincent

8
また@knite @AbuAshrafMasnun、ensure_future持っているloop引数を、その使用する理由はありませんloop.create_task以上はensure_future。そしてrun_in_executor、コルーチンでは動作しませんセマフォが代わりに使用する必要があります。
ヴィンセント

2
@vincent create_task以上に使用する理由があります。ドキュメントをensure_future参照してください。見積もりcreate_task() (added in Python 3.7) is the preferable way for spawning new tasks.
masi 2018

24

簡単な答え

  • コルーチン関数(async def)を呼び出しても実行されません。これは、ジェネレーター関数がジェネレーターオブジェクトを返すように、コルーチンオブジェクトを返します。
  • await コルーチンから値を取得します。つまり、コルーチンを「呼び出し」ます
  • eusure_future/create_task コルーチンを次の反復でイベントループで実行するようにスケジュールします(デーモンスレッドのように、それらが完了するのを待機しません)。

いくつかのコード例

最初にいくつかの用語をクリアしましょう:

  • コルーチン関数async def
  • コルーチンオブジェクト。コルーチン関数を「呼び出し」たときに得られるもの。
  • タスク、イベントループで実行するコルーチンオブジェクトにラップされたオブジェクト。

ケース1、awaitコルーチン

await1つは2つのコルーチンを作成しcreate_task、もう1つは実行に使用します。

import asyncio
import time

# coroutine function
async def p(word):
    print(f'{time.time()} - {word}')


async def main():
    loop = asyncio.get_event_loop()
    coro = p('await')  # coroutine
    task2 = loop.create_task(p('create_task'))  # <- runs in next iteration
    await coro  # <-- run directly
    await task2

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

あなたは結果を得るでしょう:

1539486251.7055213 - await
1539486251.7055705 - create_task

説明:

task1は直接実行され、task2は次の反復で実行されました。

ケース2、イベントループに制御を渡す

main関数を置き換えると、別の結果が表示されます。

async def main():
    loop = asyncio.get_event_loop()
    coro = p('await')
    task2 = loop.create_task(p('create_task'))  # scheduled to next iteration
    await asyncio.sleep(1)  # loop got control, and runs task2
    await coro  # run coro
    await task2

あなたは結果を得るでしょう:

-> % python coro.py
1539486378.5244057 - create_task
1539486379.5252144 - await  # note the delay

説明:

を呼び出すasyncio.sleep(1)と、コントロールはイベントループに戻され、ループは実行するタスクをチェックしてから、によって作成されたタスクを実行しますcreate_task

最初にコルーチン関数を呼び出すが、それを呼び出さないawaitことに注意してください。そのため、単一のコルーチンを作成し、それを実行しません。次に、もう一度コルーチン関数を呼び出し、それを呼び出しでラップしcreate_taskます。creat_taskは、実際にコルーチンをスケジュールして、次の反復で実行するようにします。したがって、結果でcreate taskは、の前に実行されawaitます。

実際、ここでのポイントは、ループに制御を戻すasyncio.sleep(0)ことです。同じ結果を確認するために使用できます。

フードの下

loop.create_task実際にを呼び出しasyncio.tasks.Task()ますloop.call_soon。そしてloop.call_soon、タスクをに配置しますloop._ready。ループの各反復中に、loop._readyのすべてのコールバックをチェックして実行します。

asyncio.waitasyncio.ensure_futureそしてasyncio.gather実際にはloop.create_task直接または間接的に呼び出します。

また、ドキュメントに注意してください:

コールバックは、登録された順に呼び出されます。各コールバックは1回だけ呼び出されます。


1
きれいな説明をありがとう!言わなければならない、それはかなりひどいデザインです。高レベルAPIが低レベルの抽象化をリークしているため、APIが複雑すぎます。
ボリス・バーコフ

1
うまく設計された骨董品プロジェクトをチェックしてください
ospider

いい説明だ!await task2呼び出しの効果は明確になったと思います。どちらの例でも、loop.create_task()呼び出しは、イベントループでtask2をスケジュールするものです。したがって、どちらのawait task2exsでも削除でき、task2は最終的に実行されます。ex2では動作は同じですが、await task2既に完了したタスク(2回目には実行されない)をスケジュールしているだけだと思いますが、ex1ではmainが完了するまでtask2が実行されないため、動作は少し異なります。違いを確認するprint("end of main")には、ex1のメインの最後に追加します
Andrew

10

https://github.com/python/asyncio/blob/master/asyncio/tasks.py#L346にリンクされたVincentによるコメント。wait()コルーチンをラップすることを示していensure_future()ます。

言い換えれば、私たちは未来が必要であり、コルーチンは静かにそれらに変換されます。

コルーチン/フューチャーをバッチ処理する方法の明確な説明を見つけたら、この回答を更新します。


コルーチンオブジェクトの場合cawait cと同等であることを意味しawait create_task(c)ますか?
アレクセイ

3

BDFL [2013]から

タスク

  • 未来に包まれたコルーチン
  • クラスTaskはクラスFutureのサブクラスです
  • だからそれはまた待つと動作します!

  • それは裸のコルーチンとどう違うのですか?
  • それを待つことなく進歩することができます
    • あなたが何か他のものを待つ限り、すなわち
      • 待って [something_else]

これを念頭に置いて、ensure_futureタスクを作成するための名前として意味があります。これは、ユーザーがそれを待っているかどうかに関係なく(何かを待っている限り)将来の結果が計算されるためです。これにより、他のものを待っている間にイベントループがタスクを完了することができます。Python 3.7 create_task将来を保証するための推奨される方法であることに注意してください。

注:ここでは、近代化のために、Guidoのスライドの「yield from」を「await」に変更しました。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.