Edited at

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

More than 1 year has passed since last update.


はじめに

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

イベントループ×非同期I/OはNodejsが採用していることで有名です。

マルチスレッド、マルチプロセスでは発生するオーバーヘッドを圧縮しつつ、多数のI/Oリクエストを捌けるアーキテクチャです。

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

しかし、Pythonのドキュメントに掲載されている、asyncioモジュールのサンプルだけでは、いまいち効果、利点が実感しにくいなと感じたので、少し改造して効果を実感できるサンプルを作ってみました。

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


サンプルを作ってみる!

asyncioでは、tcp、udpなどの通信ソケット、UNIXシグナル、パイプ処理などを非同期に処理可能です。

今回は、tcp接続で試してみます。


モデル

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

image.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つ登録します。

そして、あとで効果を測定するため

イベントループの実行開始から終了までの時間を図るようにしておきます。

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(同期接続)を測定します。

ソースはここにあります。

$ 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の場合

いよいよ。。

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