私はPython 3.6でツールを作成しています。このツールは(さまざまなエンドポイントを持つ)複数のAPIにリクエストを送信し、それらの応答を収集して解析し、データベースに保存します。
私が使用しているAPIクライアントには、URLを要求する同期バージョンがあります。たとえば、
urllib.request.Request('...
または、KennethReitzのRequests
ライブラリを使用します。
私のAPI呼び出しは、URLを要求する同期バージョンに依存しているため、プロセス全体が完了するまでに数分かかります。
ここで、API呼び出しをasync/await(asyncio)でラップしたいと思います。私はpython 3.6を使用しています。
私が見つけたすべての例/チュートリアルでは、同期URL呼び出し/ requests
を非同期バージョン(たとえば、aiohttp
)に変更する必要があります。私のコードは、私が作成していない(そして変更できない)APIクライアントに依存しているため、そのコードはそのままにしておく必要があります。
同期リクエスト(ブロッキングコード)をasync/awaitでラップして、イベントループで実行する方法はありますか?
Pythonのasyncioは初めてです。これはNodeJSでは簡単です。しかし、Pythonでこれに頭を悩ませることはできません。
解決策は、同期コードをスレッドでラップし、そのように実行することです。私はその正確なシステムを使用して、asyncio
コードを_boto3
_で実行しました(注:<python3.6を実行している場合はインラインタイプのヒントを削除してください):
_async def get(self, key: str) -> bytes:
s3 = boto3.client("s3")
loop = asyncio.get_event_loop()
try:
response: typing.Mapping = \
await loop.run_in_executor( # type: ignore
None, functools.partial(
s3.get_object,
Bucket=self.bucket_name,
Key=key))
except botocore.exceptions.ClientError as e:
if e.response["Error"]["Code"] == "NoSuchKey":
raise base.KeyNotFoundException(self, key) from e
Elif e.response["Error"]["Code"] == "AccessDenied":
raise base.AccessDeniedException(self, key) from e
else:
raise
return response["Body"].read()
_
s3.get_object()
コードの膨大な時間がI/Oの待機に費やされ、(一般的に)I/Oの待機中に費やされるため、これは機能することに注意してくださいpython GILをリリースします(GILは、一般的にpythonはお勧めできません)でスレッド化する理由です)。
_run_in_executor
_の最初の引数None
は、デフォルトのエグゼキュータで実行することを意味します。これはスレッドプールエグゼキュータですが、スレッドプールエグゼキュータを明示的に割り当てると、より明確になる可能性があります。
純粋な非同期I/Oを使用すると、数千の接続を同時に簡単に開くことができることに注意してください。スレッドプールエグゼキューターを使用すると、APIへの同時呼び出しごとに個別のスレッドが必要になります。プール内のスレッドが不足すると、スレッドが使用可能になるまで、スレッドプールは新しい呼び出しをスケジュールしません。明らかにスレッドの数を増やすことはできますが、これはメモリを消費します。数千を超えることができると期待しないでください。
同期呼び出しを非同期コードでラップする方法の説明と若干異なるコードについては、 python ThreadPoolExecutor docs )も参照してください。