今回は小ネタ。 Python の標準ライブラリには、いくつか組み込みで提供されるソケットサーバの実装がある。
例えば WSGI サーバのリファレンス実装とか。
21.4. wsgiref — WSGI ユーティリティとリファレンス実装 — Python 3.6.1 ドキュメント
HTTP サーバを動かすためのやつとか。
21.22. http.server — HTTP サーバ — Python 3.6.1 ドキュメント
ただ、上記には弱点があって、それはシングルスレッドの実装ということ。 そのため、デフォルトでは同時に複数のアクセスをさばくことができない。 これが要するにどういうことなのか、というのは次の記事なんかに書いた。
今回は、そのままだとシングルスレッドで動くソケットサーバをマルチスレッドにする方法について書く。
動作確認に使った環境は次の通り。 ただし、一応 Python 2.7 で動くことも確認はしている。
$ sw_vers ProductName: Mac OS X ProductVersion: 10.12.6 BuildVersion: 16G29 $ python --version Python 3.6.2
wsgiref.simple_server.WSGIServer
WSGIServer
クラスは WSGI サーバのリファレンス実装で、簡単なテストなんかするのに便利。
ただ、実装がシングルスレッドなので、そのままでは同時に複数のアクセスをさばくことができない。
とはいえ、まずは本当に同時に複数のアクセスをさばけないのか確認してみよう。
確認には次のサンプルコードを用いる。
ここで登場する application()
関数が WSGI アプリケーションとなっている。
これは、一つのアクセスに対してレスポンスを返すのに 5 秒もかかるように意図的に作ってある。
それを wsgiref.simple_server.make_server()
関数で作成した WSGI サーバで動かすコードとなっている。
この関数はデフォルトで組み込みで用意されているシングルスレッド実装の WSGIServer
クラスを使って起動する。
#!/usr/bin/env python # -*- coding: utf-8 -*- import time from wsgiref.simple_server import make_server def application(_environ, start_response): """レスポンスを返すまでに大きくディレイが入る WSGI アプリケーション""" start_response('200 OK', [('Content-Type', 'text/plain')]) # レスポンスが返るまで 5 秒かかる time.sleep(5) yield b'Hello, World!\n' def main(): # デフォルトではシングルスレッドの WSGIServer で立ち上がる server = make_server('localhost', 8000, application) server.serve_forever() if __name__ == '__main__': main()
動作確認
シングルスレッド云々の前に、まずは上記がちゃんと動くか、というところから確認してみよう。 ファイルとして保存したら Python で実行する。
$ python wsgisinglethread.py
次に、別の端末から curl
なんかを使ってアクセスすると、ちゃんと動くことが分かる。
もちろん、ディレイを入れた分だけレスポンスが返ってくるのにたっぷり時間がかかる。
$ curl http://localhost:8000
Hello, World!
実行時間を測る
ちゃんと動くことが分かったので、次は同時に複数のアクセスをさばけないことを確認してみよう。 今回は、時間を測るのにも Python を使うことにした。
Python の標準ライブラリにある HTTP クライアントは使うのがだるいので requests
をインストールする。
$ pip install requests
次のようなベンチマーク用のファイルを用意する。
#!/usr/bin/env python # -*- coding: utf-8 -*- import time from concurrent.futures import ThreadPoolExecutor import requests def main(): # HTTP リクエスト 2 件を並行実行するための準備をする executor = ThreadPoolExecutor(max_workers=2) parameters = ['http://localhost:8000' for _ in range(2)] # 並行実行に要した時間を測る t0 = time.time() list(executor.map(requests.get, parameters)) t1 = time.time() print('duration: {sec} sec'.format(sec=(t1 - t0))) if __name__ == '__main__': main()
先ほどのサーバを起動したまま、別の端末で実行する。 すると、処理が終わるのに 10 秒かかっており各リクエストが直列で処理されていることが分かる。
$ python benchmark.py
duration: 10.021423101425171 sec
ちなみに Jupyter Notebook を使っていれば %%time
マジックコマンドが使えるので、時間を測るのにもっと楽ができる。
%%time """time マジックコマンドを使って実行時間を測る""" # マルチスレッドで処理を実行するエグゼキュータを用意する from concurrent.futures import ThreadPoolExecutor executor = ThreadPoolExecutor(max_workers=2) # HTTP リクエスト 2 件を並行実行する import requests parameters = ['http://localhost:8000' for _ in range(2)] list(executor.map(requests.get, parameters))
実行すると、こんな感じ。
CPU times: user 7.46 ms, sys: 2.65 ms, total: 10.1 ms Wall time: 10 s
測り終わったらサーバを動かしている端末は Ctrl-C で止めよう。
マルチスレッド化する
次にシングルスレッドの実装だった WSGIServer
をマルチスレッド化してみる。
これには ThreadingMixIn
を用いる。
詳しい原理については後ほど紹介する。
次のサンプルコードでは ThreadingMixIn
を使って WSGIServer
をマルチスレッド化している。
具体的には ThreadingMixIn
と WSGIServer
を多重継承したクラス ThreadedWSGIServer
を作っている。
動かす WSGI アプリケーションについては先ほどと変わらない。
#!/usr/bin/env python # -*- coding: utf-8 -*- import time from wsgiref.simple_server import make_server from wsgiref.simple_server import WSGIServer try: # Python 3 from socketserver import ThreadingMixIn except ImportError: # Python 2 from SocketServer import ThreadingMixIn class ThreadedWSGIServer(ThreadingMixIn, WSGIServer): """マルチスレッド化した WSGIServer""" pass def application(_environ, start_response): start_response('200 OK', [('Content-Type', 'text/plain')]) # レスポンスが返るまで 5 秒かかる time.sleep(5) yield b'Hello, World!\n' def main(): server = make_server('localhost', 8000, application, # マルチスレッド化した WSGIServer を使って起動する server_class=ThreadedWSGIServer) server.serve_forever() if __name__ == '__main__': main()
先ほどと同じように、起動したら処理時間を測ってみよう。
$ python wsgimultithread.py
結果は次の通り。 先ほどとは異なって処理が 5 秒で終わっている。 これはリクエストが直列ではなく並行で処理されたことを示している。
CPU times: user 6.81 ms, sys: 2.63 ms, total: 9.43 ms Wall time: 5.01 s
このように ThreadingMixIn
を多重継承することで WSGIServer
はマルチスレッド化できる。
測定が終わったら、先ほどと同じように Ctrl-C でサーバを止める。
http.server.HTTPServer
同じように HTTPServer
についても考えてみよう。
次のサンプルコードでは、先ほどと同じようにレスポンスまで 5 秒かかるハンドラをシングルスレッドの HTTPServer
で動かしている。
#!/usr/bin/env python # -*- coding: utf-8 -*- import time try: # Python 3 from http.server import BaseHTTPRequestHandler from http.server import HTTPServer except ImportError: # Python 2 from BaseHTTPServer import BaseHTTPRequestHandler from BaseHTTPServer import HTTPServer class GreetingHandler(BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.send_header('Content-Type', 'text/plain') self.end_headers() # Content-Body の送信まで 5 秒のディレイが入る time.sleep(5) self.wfile.write(b'Hello, World!\r\n') def main(): server_address = ('localhost', 8000) # シングルスレッドな HTTPServer を使って起動する httpd = HTTPServer(server_address, GreetingHandler) httpd.serve_forever() if __name__ == '__main__': main()
これまでと同じように、起動したら実行時間を測ってみよう。
$ python httpsinglethread.py
次のように 10 秒かかることが分かった。 やはり、リクエストが直列に処理されてしまっている。
CPU times: user 7.93 ms, sys: 3.16 ms, total: 11.1 ms Wall time: 10 s
マルチスレッド化する
次に HTTPServer
クラスをマルチスレッド化する。
やり方は、先ほどの WSGIServer
と変わらない。
ThreadingMixIn
と一緒に多重継承するだけ。
次のサンプルコードではマルチスレッド化した ThreadedHTTPServer
で時間のかかるハンドラを動かしている。
#!/usr/bin/env python # -*- coding: utf-8 -*- import time try: # Python 3 from http.server import BaseHTTPRequestHandler from http.server import HTTPServer from socketserver import ThreadingMixIn except ImportError: # Python 2 from BaseHTTPServer import BaseHTTPRequestHandler from BaseHTTPServer import HTTPServer from SocketServer import ThreadingMixIn class GreetingHandler(BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.send_header('Content-Type', 'text/plain') self.end_headers() # Content-Body の送信まで 5 秒のディレイが入る time.sleep(5) self.wfile.write(b'Hello, World!\r\n') class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): """マルチスレッド化した HTTPServer""" pass def main(): server_address = ('localhost', 8000) # マルチスレッド化した HTTP サーバを使う httpd = ThreadedHTTPServer(server_address, GreetingHandler) httpd.serve_forever() if __name__ == '__main__': main()
起動して、処理にかかる時間を測ってみよう。
$ python httpmultithread.py
次のように 5 秒で終わったことから、リクエストが並行処理されたことが分かる。
CPU times: user 7.1 ms, sys: 2.92 ms, total: 10 ms Wall time: 5.01 s
ThreadingMixIn を使ったマルチスレッド化の原理について
それでは、次にどうして ThreadingMixIn
を多重継承することで、前述した WSGIServer
や HTTPServer
がマルチスレッド化できたのかについて見ていく。
まずは ThreadingMixIn
のソースコードを確認すると process_request()
というメソッドを実装していることが分かる。
https://github.com/python/cpython/blob/v3.6.2/Lib/socketserver.py#L645
上記のメソッドでは受け取った request
を、新たに生成した threading.Thread
で処理していることが読み取れる。
これが WSGIServer
や HTTPServer
の process_request()
メソッドをオーバーライドしていたわけだ。
では process_request()
は、どこで定義されているかというと、以下の BaseServer
クラス内に見つかる。
https://github.com/python/cpython/blob/v3.6.2/Lib/socketserver.py#L157
次の場所に process_request()
があって docstring にも、これは ForkingMixIn
や ThreadingMixIn
で上書きされる、とある。
https://github.com/python/cpython/blob/v3.6.2/Lib/socketserver.py#L342
ForkingMixIn
というのは初めて登場したけど、マルチプロセスを使った並列化をするのに使われるクラスのこと。
そして WSGIServer
や HTTPServer
は BaseServer
を継承して作られている。
それぞれの process_request()
メソッドを ThreadingMixIn
がオーバーライドすることでマルチスレッド化されたわけだ。
>>> from socketserver import BaseServer >>> from wsgiref.simple_server import WSGIServer >>> issubclass(WSGIServer, BaseServer) True >>> from http.server import HTTPServer >>> issubclass(HTTPServer, BaseServer) True
これで ThreadingMixIn
を使ってマルチスレッド化ができる理由がわかった。
まとめ
BaseServer
を継承して作られた Python 組み込みのソケットサーバは ThreadingMixIn
を多重継承することでマルチスレッド化できる。
スマートPythonプログラミング: Pythonのより良い書き方を学ぶ
- 作者: もみじあめ
- 発売日: 2016/03/12
- メディア: Kindle版
- この商品を含むブログ (1件) を見る