#Pythonでノンブロッキング通信で非同期処理を実現する
ある処理が完了する前に別の処理を実行する方法として、マルチプロセス、マルチスレッド、ノンブロッキングがある。プロセスは仮想メモリといった固有のメモリを持つ処理単位になり、プロセス間同士でメモリが共有されない。スレッドはプロセス内での処理単位となるため、同じプロセス内のスレッドはメモリが共有される。
ノンブロッキングは1スレッドで複数のリクエストに対して応答することができる。
##ブロッキング通信とノンブロッキング通信
ソケット通信では「ブロッキング通信」と「ノンブロッキング通信」の2つの通信方法がある。ブロッキングは送受信の完了を待ってから他の処理を開始する通信方法のため、逐次的な処理をする時に利用することが多い。一方、ノンブロッキングは通信が完了していなくても他の処理の開始ができる通信方法のため、非同期処理をする時に利用することが多い。
##Python3.5でのノンブロッキング
Python3.4から標準ライブラリにasyncio
というモジュールが追加され、ノンブロッキング処理ができるようになった(参考)。
asyncio
についてはこちらに非常にわかりやすくまとまっていて、サンプルも多数掲載されている。
##uWSGIでノンブロッキングモード
uWSGI1.9
から、こちらに記載されているようにノンブロッキングモードがサポートされるようになった。
また、2.0.4からasyncio
がサポートされた(参考)。
##環境構築
以下、Python3.5がインストールされている環境で実行する。
-
greenlet
のインストール
$ pip3 install greenlet
-
asyncio
がサポートされたuWSGI
をインストールする
まずは、greenlet
がインストールされているディレクトリを探す。
$ find / -name greenlet -type d
Mac OS
の環境では、結果が/Users/xxx/.pyenv/versions/3.5.0/include/python3.5m/greenlet
であったので、下記を実行。
$ CFLAGS="-I/Users/xxx/.pyenv/versions/3.5.0/include/python3.5m" UWSGI_PROFILE="asyncio" pip3 install uwsgi
##動作検証
ブロッキングとノンブロッキングで簡単な動作検証をする。
サンプルコードではファイル読み込みをしているが、そのファイルのは下記を利用した。
zero
one
two
three
four
five
また、検証では、Apache Bench
を利用したが、ローカルを利用する場合、こちらに記載があるように注意が必要で、今回は下記のコマンドで実施。
$ ab -n 10000 -c 100 http://127.0.0.1:9090/
###ブロッキングでの動作検証
※この場合、uWSGI
をpip3 install uwsgi
でインストールする必要がある。既にノンブロッキングモードでインストールしている場合、下記でいったんuWSGI
をアンインストールする。
$ pip3 unistall uwsgi
####サンプル
読み込んだファイルの内容をログ出力する。
def application(environ, start_response):
def my_generator(name):
with open(name) as lines:
yield from lines
g = my_generator("numbers.txt")
for k, v in enumerate(g):
print("%s:%s" % (k, v), end="")
start_response('200 OK', [('Content-Type','text/html')])
return [b"Hello World"]
####検証
プロセスとスレッド数を1に指定して実行。
$ uwsgi --http-socket :9090 --processes 1 --threads 1 --logto uwsgi.log --wsgi-file webapp.py
####結果
Apache Bench
の結果(一部のみ)。
Requests per second: 2694.06 [#/sec] (mean)
Time per request: 37.119 [ms] (mean)
Time per request: 0.371 [ms] (mean, across all concurrent requests)
Transfer rate: 144.70 [Kbytes/sec] received
###ノンブロキングでの動作
非同期で5秒後に読み込んだファイルの内容をログ出力。
####サンプル
import asyncio
@asyncio.coroutine
def my_generator(name):
with open(name) as lines:
yield from lines
def read():
g = my_generator("numbers.txt")
for k, v in enumerate(g):
print("%s:%s" % (k, v), end="")
def application(environ, start_response):
asyncio.get_event_loop().call_later(5, read)
start_response('200 OK', [('Content-Type','text/html')])
return [b"Hello World"]
####検証
ブロッキングのときと同様にプロセスとスレッド数を1に指定して実行。
$ uwsgi --asyncio 2 --http-socket :9090 --greenlet --processes 1 --threads 1 --logto uwsgi.log --wsgi-file webapp.py
ここで、--asyncio
に2
を指定しているが、1
にすると下記が表示され、起動しなかった。
the greenlet suspend engine requires async mode
async mode
について調べてみると下記の記述にあるように、コアという、リクエスト単位のデータを格納するメモリ構造(スレッドの概念の紛らわしいのでコアと定義されている)があるとのこと。
Technically, cores are simple memory structures holding request’s data, but to give the user
the illusion of a multithreaded system we use that term.
これらのコアは切り替える必要があって、2個以上ないとだめなのだろう。
Each core can manage a single request, so the more core you spawn, more requests you will be
able to manage (and more memory you will use). The job of the suspend/resume engines is to stop
the current request management, move to another core, and eventually come back to the old one
(and so on).
####結果
ブロッキングのときと比較して、今回のサンプルではパフォーマンス面で若干遅い。
他のサンプル等で比較したほうがいいと思われる。
equests per second: 2336.16 [#/sec] (mean)
Time per request: 42.805 [ms] (mean)
Time per request: 0.428 [ms] (mean, across all concurrent requests)
Transfer rate: 125.48 [Kbytes/sec] received
ノンブロッキングモードで2コアを利用しているので、uWSGI
を再インストールし、スレッド数を2で確認してみた。その結果は以下。Mac環境の問題かもしれない。
Requests per second: 2691.94 [#/sec] (mean)
Time per request: 37.148 [ms] (mean)
Time per request: 0.371 [ms] (mean, across all concurrent requests)
Transfer rate: 144.59 [Kbytes/sec] received
##備考
asyncio
についてもう少し研究し、またFlask
といったフレームワークを導入し、Nginxも使うなどある程度の条件下で、研究を兼ねた検証を続けたい。また、Golangとも比較するなど他の言語でも試していく。
こちらにIf you are in doubt, do not use async mode.
と記載があるが、まだまだ実用レベルには至っていないだけなのだろうか。それとも然るべきやる方があるのだろうか、深掘りしていこう。