Python3のasync/await構文を使用した非同期HTTPリクエストの実装

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文を使用して自動的にクローズする方法の両方を示しています。

タグ: Python asyncio aiohttp 非同期処理 コルーチン

7月24日 18:11 投稿