python3 asyncioの効果を実感したい！

# はじめに

Python3では、イベントループ、非同期IOの標準モジュールとして「`asyncio`」があります。

イベントループ×非同期I/OはNodejsが採用していることで有名です。
マルチスレッド、マルチプロセスでは発生するオーバーヘッドを圧縮しつつ、多数のI/Oリクエストを捌けるアーキテクチャです。

ということは頭でわかっていても、実際「おおすごい！」と実感したいところです。

しかし、[Pythonのドキュメント](https://docs.python.jp/3/library/asyncio.html)に掲載されている、`asyncio`モジュールのサンプルだけでは、いまいち効果、利点が実感しにくいなと感じたので、少し改造して効果を実感できるサンプルを作ってみました。

※ `aiohttp` とかでも実感はできるのですが、実際asyncioがどう使われているかまではわからないのでやってみました。

# サンプルを作ってみる！

asyncioでは、tcp、udpなどの通信ソケット、UNIXシグナル、パイプ処理などを非同期に処理可能です。
今回は、tcp接続で試してみます。

## モデル

3つのtcpサーバーにリクエストを送信し、結果を取得するクライアントというモデルでサンプルを書いてみました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/26693/dfa41af3-4ec7-ebb2-7925-32453fbd2e58.png)

## サーバー側

tcpでリクエストを待ち受け、送信された内容をそのまま送り返すechoサーバーを作っておきます。
その際、1sの待ちをいれ、応答に1sかかるサーバーとしておきます。
※これはあとで効果を測定するときにわかりやすくするためです。

ソースはgithubにあげておきました。
※実際、コードのほとんどはPythonドキュメントのサンプルとなってます

https://github.com/n-someya/my-asyncio-study/blob/master/tcp_echo_sleep_server.py

## クライアント側

以下のPythonドキュメントのサンプルから流用します。

https://docs.python.jp/3/library/asyncio-stream.html#tcp-echo-client-using-streams

ドキュメントの例だと1回しかtcp接続を実行しないため、イベントループ内で3回呼び出しを行うようコードを加えました。
以下の `multiple_request` の部分がそれに該当します。
`asyncio.wait` を使い、イベントループに `tcp_echo_client` を3つ登録します。

そして、あとで効果を測定するため
イベントループの実行開始から終了までの時間を図るようにしておきます。

```python
async def tcp_echo_client(message, port, loop):
    """
    async tcp client
    Args:
        message: send strings
        port: send tcp request to 127.0.0.0:port
        loop: event loop
    """
    # Asyncio でコネクションをオープン
    # オープン中は別の処理にスイッチされ別の処理が実行可能
    print("open connection....")
    reader, writer = await asyncio.open_connection('127.0.0.1', port,
                                                        loop=loop)
    # サーバー側にデータ送信
    print('Send: %r' % message)
    writer.write(message.encode())
    # Asyncio でサーバー側からデータ受信
    # データ受信中は別の処理にスイッチされ別の処理が実行可能
    data = await reader.read(3072)
    print('Received: %r' % data.decode())

    # コネクションクローズ
    print('Close the socket')
    writer.close()


async def multiple_request(loop):
    """
    ポート番号 8888, 8889, 8890 にデータ送信する3つのタスクを
    イベントループに登録する

    Args:
        loop: event loop
    """
    tasks = []
    port = 8888
    for i in range(0, LOOP):
        message = 'Hello World! {0}'.format(random.randint(0, 100)) 
        tasks.append(tcp_echo_client(message, port, loop))
        port += 1
    done, pending = await asyncio.wait(tasks)


loop = asyncio.get_event_loop()
start = datetime.datetime.now()
# イベントループにより処理を開始する。
loop.run_until_complete(multiple_request(loop))
end = datetime.datetime.now()
loop.close()
print("execution time: {0}".format(end - start))
```

# 効果を計ってみた

echoサーバーを8888～8890のポート番号で、3つ立ち上げておきます。

## 非asyncioの場合

まずは、非asyncio(同期接続)を測定します。
ソースは[ここ](https://github.com/n-someya/my-asyncio-study/blob/master/tcp_echo_client_sync.py)にあります。

```shell
$ python tcp_echo_client_sync.py
Open: connection....
Send: 'Hello World! 76'
Received: 'Hello World! 76'
Close: tcp socket
Open: connection....
Send: 'Hello World! 96'
Received: 'Hello World! 96'
Close: tcp socket
Open: connection....
Send: 'Hello World! 22'
Received: 'Hello World! 22'
Close: tcp socket
Execution time: 0:00:03.069013
```

予想通りの結果です。
応答に1sかかるサーバーに3回接続しているので、だいたい3sの実行時間となっています。

## asyncioの場合

いよいよ。。

```shell
$ python tcp_echo_client_asyncio.py
open connection....
open connection....
open connection....
Send: 'Hello World! 21'
Send: 'Hello World! 55'
Send: 'Hello World! 10'
Received: 'Hello World! 55'
Close the socket
Received: 'Hello World! 21'
Close the socket
Received: 'Hello World! 10'
Close the socket
execution time: 0:00:01.049011
```

全体の実行時間がだいたい1sになりました！

## 結果解説

さらっと解説します。
イベントループ内では、`await`されるとコンテキストスイッチのようなことが起こります。
今回の例だと、

8888番ポートへの処理で `data = await reader.read(3072)` がされる
↓
イベントループにより、次の処理へと移る
↓
8889番ポートへの処理で `data = await reader.read(3072)` がされる
↓
イベントループにより、次の処理へと移る
↓
....

となり、8888番からのレスポンス待ちにより、他の処理がブロックされることなく実行されていったため、時間が短縮されていると考えられます。