web-dev-qa-db-ja.com

asyncio:エグゼキュータの非同期関数から結果を収集する

多数のHTTPリクエストを開始し、すべてが戻ってきたら結果を収集したいと思います。 asyncioを使用すると、非ブロッキング方式でリクエストを送信できますが、結果の収集に問題があります。

私はこの特定の問題のために作られた aiohttp のような解決策を知っています。しかし、HTTPリクエストは単なる例であり、私の質問はasyncioを正しく使用する方法です。

サーバー側には、_localhost/_へのすべてのリクエストに「HelloWorld!」で応答するflaskがありますが、応答するまで0.1秒待機します。すべての例で、私はm 10個のリクエストを送信します。同期コードには約1秒かかりますが、非同期バージョンでは0.1秒で完了します。

クライアント側では、同時に多くのリクエストをスピンアップして結果を収集したいと思います。私はこれを3つの異なる方法で行おうとしています。 asyncioはブロックコードを回避するためにエグゼキュータを必要とするため、すべてのアプローチは_loop.run_in_executor_を呼び出します。

このコードはそれらの間で共有されます:

_import requests
from time import perf_counter
import asyncio

loop = asyncio.get_event_loop()

async def request_async():
    r = requests.get("http://127.0.0.1:5000/")
    return r.text

def request_sync():
    r = requests.get("http://127.0.0.1:5000/")
    return r.text
_

アプローチ1:

タスクのリストでasyncio.gather()を使用してから、_run_until_complete_を使用します。 Asyncio.gather vs asyncio.wait を読んだ後、gatherは結果を待つように見えました。しかし、そうではありません。したがって、このコードは、リクエストが終了するのを待たずに、即座に戻ります。ここでブロッキング機能を使用すると、これは機能します。非同期関数を使用できないのはなぜですか?

_# approach 1
start = perf_counter()
tasks = []
for i in range(10):
    tasks.append(loop.run_in_executor(None, request_async)) # <---- using async function !

gathered_tasks = asyncio.gather(*tasks)
results = loop.run_until_complete(gathered_tasks)
stop = perf_counter()
print(f"finished {stop - start}") # 0.003

# approach 1(B)
start = perf_counter()
tasks = []
for i in range(10):
    tasks.append(loop.run_in_executor(None, request_sync)) # <---- using sync function

gathered_tasks = asyncio.gather(*tasks)
results = loop.run_until_complete(gathered_tasks)

stop = perf_counter()
print(f"finished {stop - start}") # 0.112
_

Pythonは、_coroutine "request_async"_が待たされなかったことを警告しています。この時点で、実用的な解決策があります。エグゼキュータで通常の(非同期ではない)関数を使用します。しかし、私はasync関数定義で機能するソリューションが欲しいです。それらの中でawaitを使用したいので(この単純な例では必要ありませんが、さらにコードをasyncioに移動すると、それが重要になると確信しています)。

アプローチ2:

Pythonは、私のコルーチンが決して待たれないことを警告します。だから彼らを待ちましょう。アプローチ2は、すべてのコードを外部の非同期関数にラップし、収集の結果を待ちます。同じ問題、また即座に返されます(これも同じ警告):

_# approach 2
async def main():

    tasks = []
    for i in range(10):
        tasks.append(loop.run_in_executor(None, request_async))

    gathered_tasks = asyncio.gather(*tasks)

    return await gathered_tasks # <-------- here I'm waiting on the coroutine 

start = perf_counter()
results = loop.run_until_complete(main())
stop = perf_counter()
print(f"finished {stop - start}")  # 0.0036
_

これは本当に私を混乱させました。 gatherの結果を待っています。直感的に、それは私が集めているコルーチンに伝播されるべきです。しかし、pythonは、私のコルーチンが決して待たれないと不平を言っています。

私はもう少し読んで見つけました: どうすればasyncioでリクエストを使用できますか?

これはまさに私の例です:requestsasyncioの組み合わせ。それは私をアプローチ3に導きます:

アプローチ3:

アプローチ2と同じ構造ですが、run_in_executor()に個別に与えられた各タスクを待機します(確かに、これはコルーチンを待機していると見なされます)。

_# approach 3:
# wrapping executor in coroutine
# awaiting every task individually
async def main():

    tasks = []
    for i in range(10):
        task = loop.run_in_executor(None, request_async)
        tasks.append(task)

    responses = []
    for task in tasks:
        response = await task
        responses.append(response)

    return responses

start = perf_counter()
results = loop.run_until_complete(main())
stop = perf_counter()

print(f"finished {stop - start}") # 0.004578
_

私の質問は、コルーチンにブロッキングコードを入れて、エグゼキュータと並行して実行したいということです。結果を取得するにはどうすればよいですか?

3
lhk

私の質問は、コルーチンにブロッキングコードを入れて、エグゼキュータと並行して実行したいということです。結果を取得するにはどうすればよいですか?

答えは、コルーチンにブロッキングコードがあるはずがないということです。必要な場合は、run_in_executorを使用して分離する必要があります。したがって、(requestsを使用して)request_asyncを記述する唯一の正しい方法は次のとおりです。

async def request_async():
    loop = asyncio.get_event_loop()
    return await loop.run_in_executor(None, request_sync)

request_asyncrun_in_executorに与えることは、run_in_executorの-​​point全体が別のスレッドでsync関数を呼び出すことであるために運命づけられています。コルーチン関数を指定すると、(別のスレッドで)喜んで呼び出し、返されたコルーチンオブジェクトを「結果」として提供します。これは、通常の関数を期待するコードにジェネレーターを渡すのと同じです-はい、ジェネレーターを正常に呼び出しますが、返されたオブジェクトをどう処理するかはわかりません。

さらに重要なことは、asyncdefの前に置いて、使用可能なコルーチンを期待することはできないということです。コルーチンは、他の非同期コードを待つ場合を除いて、ブロックしてはなりません。

これで、使用可能なrequest_asyncができたら、次のように結果を収集できます。

async def main():
    tasks = [request_async() for _i in range(10)]
    results = await asyncio.gather(*tasks)
    return results

results = loop.run_until_complete(main())
8
user4815162342