memo

2016-05-29

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() が呼ばれた後に別のコールバックが呼ばれているように見える。 この辺ちゃんとコード読んで動作を確認したほうがよさそう。)


気が向いたら続く。