Pythonにおける並行処理にはいくつかの選択肢があります。マルチプロセスは複数のコアを利用できますがリソース消費が大きく、スレッドはグローバルインタプリタロック(GIL)のため複数コアを活かせません。これらの制約から、コルーチンが好まれることが多いです。従来のコルーチンはジェネレータに基づいており、サードパーティライブラリなしでは実装コストと学習コストが高くなりました。一般的に使用されるのはGreenletベースのGeventですが、Python3.5以降ではネイティブなコルーチン構文が提供され、より簡単に利用できるようになりました。この新しい構文はasyncやawaitといった新しいキーワードを使用し、通常のPython構文とは少し異なります。
以下に、コルーチンを使用して非同期にHTTPリクエストを行う例を示します。
import asyncio
import traceback
import aiohttp
BASE_URL = "https://github.com/"
async def fetch_url(url):
session = aiohttp.ClientSession()
try:
response = await session.get(url)
await session.close()
return response
except Exception:
await session.close()
return traceback.format_exc()
async def send_post(url, payload=None):
async with aiohttp.ClientSession() as session:
try:
async with session.post(url, data=payload) as response:
return response
except Exception:
return traceback.format_exc()
async def process_url(target_url=BASE_URL):
print(f"{target_url}===処理を開始します...")
response = await fetch_url(target_url)
print(f"{target_url}===処理が完了しました")
print(response)
if __name__ == '__main__':
target_urls = [BASE_URL, "https://www.baidu.com", "https://www.zhihu.com"]
event_loop = asyncio.get_event_loop()
tasks = [asyncio.ensure_future(process_url(url)) for url in target_urls]
event_loop.run_until_complete(asyncio.wait(tasks))
event_loop.close()
実行結果は以下のようになります:
https://github.com/===処理を開始します...
https://www.baidu.com/===処理を開始します...
https://www.zhihu.com/===処理を開始します...
https://www.baidu.com/===処理が完了しました
<ClientResponse(https://www.baidu.com) [200 OK]>
<CIMultiDictProxy('Content-Type': 'text/html', 'Content-Length': '81', ...)>
https://www.zhihu.com/===処理が完了しました
<ClientResponse(https://www.zhihu.com/signin?next=/) [200 OK]>
<CIMultiDictProxy('Server': 'CLOUD ELB 1.0.0', 'Content-Type': 'text/html; charset=utf-8', ...)>
https://github.com/===処理が完了しました
Traceback (most recent call last):
File "async_example.py", line 12, in fetch_url
response = await session.get(url)
...
aiohttp.client_exceptions.ClientOSError: [WinError 121] セマフォがタイムアウトしました
この結果からわかるように、同じスレッド内でリクエストが順番に実行されるのではなく、I/O待ちが発生したときに自動的に切り替わっています。GitHubへのリクエストが最初に開始されましたが、タイムアウトエラーにより最後に完了しています。これにより、新しいasync/await構文が非同期I/Oを効果的に処理できることが確認できます。
上記の例では、POSTとGETリクエストを異なる方法で実装しました。これは、aiohttpのClientSessionがファイルと同様にI/Oコンテキストであり、クローズする必要があるためです。fetch_url関数のように手動でclose()を呼び出す方法と、send_post関数のようにwith文を使用して自動的にクローズする方法の両方を示しています。