python の asyncio について調べる
今まで何となく使ってみていた asyncio だけど、一度基礎から調べてみようという気になった。
基礎
この手のイベント駆動ネットワーキングライブラリで基礎といえば、 「あるファイルディスクリプタが読み・書き可能になったらコールバック関数を呼び出す」 という辺りの動作だろう。
asyncio では、 Event Loop が そういうメソッド群 を持っている。
試す
まずは動作確認用のサーバーを用意する。 取り敢えずテキトウにスリープしつつ hello world
と返すだけの簡単なネットワークサーバーを作った。
hello-server.py
:
import socket import threading import time def hello(client): time.sleep(1) client.sendall(b'hello world\n') time.sleep(1) client.sendall(b'hello world\n') time.sleep(1) client.sendall(b'hello world\n') client.close() def main(): sock = socket.socket() sock.bind(('127.0.0.1', 9999)) sock.listen(1) while True: client, addr = sock.accept() threading.Thread(target=hello, args=(client, )).start() if __name__ == '__main__': main()
このサーバーに接続し返ってきたデータを画面に出力する、というクライアントを上記メソッドを利用して実装する。
hello-client1.py
:
import asyncio import socket import time # socket モジュールを用いて、サーバーに普通に接続する sock = socket.socket() sock.connect(('127.0.0.1', 9999)) # コールバック関数の用意 def reader(): # 動作が分かりやすいよう、現在時刻も画面に出力しておく print(int(time.time())) # ソケットからデータを読み出し、画面に出力する data = sock.recv(1024) print(repr(data)) # データが取れなかった == ソケットが閉じられた場合は、 # event loop を停止し、プログラムを終了する if not data: loop.stop() print('end') loop = asyncio.get_event_loop() # Event loop にソケットのファイルディスクリプタと、 # それが読みだし可能になった時に呼び出すべきコールバック関数を登録する loop.add_reader(sock.fileno(), reader) # Event loop を走らせる loop.run_forever()
実行してみる:
$ python3.5 hello-client1.py
1464530319
b'hello world\n'
end
1464530320
b'hello world\n'
end
1464530321
b'hello world\n'
end
1464530321
b''
end
ちゃんと1秒毎 (ソケットが読みだし可能になるごと) に、コールバック関数が呼ばれている。
複数ソケットを同時に扱ってみる
複数のソケットを同時に event loop に登録した場合の動作も見てみる。
hello-client2.py
:
import asyncio import socket import time # コールバック関数の用意 # 今回は複数のソケットを扱うので、 # 動作判別用の ID 値と socket を引数で受け取るようにする。 def reader(id, sock): # 動作が分かりやすいよう、コールバック関数の ID 値も含めて print する print(id, int(time.time())) data = sock.recv(1024) print(id, repr(data)) # XXX: 本当は両方のソケットが閉じられたことを確認して終了するべき if not data: loop.stop() print(id, 'end') loop = asyncio.get_event_loop() # 複数同時に接続する sock1 = socket.socket() sock1.connect(('127.0.0.1', 9999)) sock2 = socket.socket() sock2.connect(('127.0.0.1', 9999)) # それぞれのソケットとコールバック関数を event loop に登録する。 # ID 値とソケットがコールバック関数に引数として渡るよう、 # add_reader() の *args として引き渡す。 loop.add_reader(sock1.fileno(), reader, 1, sock1) loop.add_reader(sock2.fileno(), reader, 2, sock2) # Event Loop を走らせる loop.run_forever()
実行してみる:
$ python3.5 hello-client2.py
1 1464530576
1 b'hello world\n'
1 end
2 1464530576
2 b'hello world\n'
2 end
1 1464530577
1 b'hello world\n'
1 end
2 1464530577
2 b'hello world\n'
2 end
1 1464530578
1 b'hello world\n'
1 end
1 1464530578
1 b''
1 end
2 1464530578
2 b'hello world\n'
2 end
きちんと複数のソケットを同時に監視できている。
ソケットが読みだし可能になると、まず片方のコールバック関数が呼び出される。 関数が終了すると処理が event loop に戻り、そこで改めて次のコールバック関数が呼び出される。 (複数のコールバック関数が同時に呼び出されるということはない。)
(終了時の処理、
loop.stop()
が呼ばれた後に別のコールバックが呼ばれているように見える。 この辺ちゃんとコード読んで動作を確認したほうがよさそう。)
気が向いたら続く。