これは、なにをしたくて書いたもの?
Pythonには、TCPでサーバーを書けるTCPServer、HTTPでサーバーを書けるHTTPServerがあるのですが(そのまま)、これらが
シングルスレッドで動作しているので、マルチスレッドにするには?ということで調べてみました。
環境
今回の環境は、こちら。
$ python3 -V Python 3.6.8
TCPServer
まずは、ふつうにTCPServerを使ってみましょう。お題は、Echoで。
echo_server.py
from socketserver import TCPServer, BaseRequestHandler address = ('localhost', 5000) class EchoHandler(BaseRequestHandler): def handle(self): message = self.request.recv(1024) self.request.sendall(b'Reply: ' + message) TCPServer.allow_reuse_address = True # avoid, OSError: [Errno 98] Address already in use with TCPServer(address, EchoHandler) as server: server.serve_forever()
あんまり本体とは関係ないですが、以下の1文を入れているのは短い間隔でサーバーを再起動した時に、ポートバインドで失敗させないためです。
TCPServer.allow_reuse_address = True # avoid, OSError: [Errno 98] Address already in use
BaseServer / allow_reuse_address
それ以外にSocketのオプションを触りたかったら、TCPServerのserver_bindメソッドをオーバーライドしましょう。
https://github.com/python/cpython/blob/v3.6.8/Lib/socketserver.py#L462-L471
脱線しました。
確認しましょう。サーバーを起動。
$ python3 echo_server.py
ncで確認。
$ echo 'Hello' | nc localhost 5000 Reply: Hello
とりあえず、これが基本形です。
ThreadingTCPServerを使う
では、先ほどのEchoサーバーをマルチスレッドにしてみます。
変更箇所は少なくて、TCPServerをThreadingTCPServerにするだけです。
socketserver.ThreadingTCPServer
threading_echo_server.py
from socketserver import ThreadingTCPServer, BaseRequestHandler import threading address = ('localhost', 5000) class EchoHandler(BaseRequestHandler): def handle(self): thread_name = threading.current_thread().getName() prefix = '[{}] Reply: '.format(thread_name) message = self.request.recv(1024) self.request.sendall(prefix.encode('utf-8') + message) ThreadingTCPServer.allow_reuse_address = True with ThreadingTCPServer(address, EchoHandler) as server: server.serve_forever()
こちらも忘れずに。
ThreadingTCPServer.allow_reuse_address = True
あと、レスポンスの内容にスレッド名を入れておきました。
起動。
$ python3 threading_echo_server.py
確認。
$ echo 'Hello' | nc localhost 5000 [Thread-1] Reply: Hello $ echo 'Hello' | nc localhost 5000 [Thread-2] Reply: Hello $ echo 'Hello' | nc localhost 5000 [Thread-3] Reply: Hello $ echo 'Hello' | nc localhost 5000 [Thread-4] Reply: Hello $ echo 'Hello' | nc localhost 5000 [Thread-5] Reply: Hello $ echo 'Hello' | nc localhost 5000 [Thread-6] Reply: Hello $ echo 'Hello' | nc localhost 5000 [Thread-7] Reply: Hello $ echo 'Hello' | nc localhost 5000 [Thread-8] Reply: Hello $ echo 'Hello' | nc localhost 5000 [Thread-9] Reply: Hello $ echo 'Hello' | nc localhost 5000 [Thread-10] Reply: Hello
スレッドが使われていることがわかりますが、どんどんスレッドの名前が変わっていってますね。
素直に、毎回スレッドを作ってstartしているみたいです。
https://github.com/python/cpython/blob/v3.6.8/Lib/socketserver.py#L662-L663
ところで余談ですが、マルチスレッドではなく、マルチプロセスなTCPServerもあるようです。
ThreadPoolExecutorを使う
ThreadingTCPServerでもいいのですが、スレッドを絞りたい時などはThreadPoolExecutorが使えた方がいいかもしれません。
ThreadPoolExecutorを使うようなTCPServerはないので、簡単に自分で作ってみました。
threadpool_echo_server.py
from concurrent.futures import ThreadPoolExecutor from socketserver import ThreadingTCPServer, BaseRequestHandler import threading address = ('localhost', 5000) workers = 3 class EchoHandler(BaseRequestHandler): def handle(self): thread_name = threading.current_thread().getName() prefix = '[{}] Reply: '.format(thread_name) message = self.request.recv(1024) self.request.sendall(prefix.encode('utf-8') + message) class ThreadPoolTCPServer(ThreadingTCPServer): def __init__(self, address, handler, workers): super().__init__(address, handler) self.executor = ThreadPoolExecutor(max_workers = workers) def process_request(self, request, client_address): self.executor.submit(self.process_request_thread, request, client_address) def server_close(self): super().server_close() self.executor.shutdown(wait = True) ThreadPoolTCPServer.allow_reuse_address = True with ThreadPoolTCPServer(address, EchoHandler, workers) as server: server.serve_forever()
ThreadingTCPServerクラスを継承したクラスを作成。
class ThreadPoolTCPServer(ThreadingTCPServer):
このあたりを見つつ、process_request、server_closeメソッドをオーバーライドすれば良さそうです。
https://github.com/python/cpython/blob/v3.6.8/Lib/socketserver.py#L660-L678
オーバーライドしているメソッドが定義してあるのは、ThreadingMixInです。
スレッド数は、3つにしてみました。
workers = 3
確認。
$ python3 threadpool_echo_server.py
結果。
$ echo 'Hello' | nc localhost 5000 [ThreadPoolExecutor-0_0] Reply: Hello $ echo 'Hello' | nc localhost 5000 [ThreadPoolExecutor-0_1] Reply: Hello $ echo 'Hello' | nc localhost 5000 [ThreadPoolExecutor-0_0] Reply: Hello $ echo 'Hello' | nc localhost 5000 [ThreadPoolExecutor-0_1] Reply: Hello $ echo 'Hello' | nc localhost 5000 [ThreadPoolExecutor-0_2] Reply: Hello $ echo 'Hello' | nc localhost 5000 [ThreadPoolExecutor-0_0] Reply: Hello
OKそうです。
まあ、元のソースを見てオーバーライドしているので、バージョンに合わせてソースコードを確認した方が良さそうですが…。
通常は、ThreadingTCPServerでいいのかな、と。
HTTPServer
続いて、HTTPServer。
21.22. http.server --- HTTP サーバ — Python 3.6.9 ドキュメント
よく、こうやって起動するあれです。
$ python3 -m http.server Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
これを、プログラムから使います。
http_server.py
from http.server import HTTPServer, SimpleHTTPRequestHandler address = ('localhost', 8080) with HTTPServer(address, SimpleHTTPRequestHandler) as server: server.serve_forever()
SimpleHTTPRequestHandlerというのは、カレントディレクトリ配下のファイルを公開するHandlerです。
http.server.SimpleHTTPRequestHandler
https://github.com/python/cpython/blob/v3.6.8/Lib/http/server.py#L1175-L1211
これで、「python3 -m http.server」と同義です。
$ python3 http_server.py
ちゃんと自分でリクエストを処理するコードを書く場合は、BaseHTTPRequestHandlerクラスを継承したHandlerを作成するようですが、
今回は簡単に済ませました…。
http.server.BaseHTTPRequestHandler
動作確認は、省略。
ところで、「allow_reuse_address」の指定がありませんが、HTTPServerの場合はデフォルトでTrue(1)になっています。
https://github.com/python/cpython/blob/v3.6.8/Lib/http/server.py#L132
HTTPServerをマルチスレッドにしたい
HTTPServerをマルチスレッドにするのは、Python 3.7かそれ以前で方法が変わります。
今回はPython 3.6を使っているので、ThreadingTCPServerを使って、HandlerだけHTTPのものを使うことになります。
threading_http_server.py
from socketserver import ThreadingTCPServer, BaseRequestHandler from http.server import SimpleHTTPRequestHandler address = ('localhost', 8080) ThreadingTCPServer.allow_reuse_address = True with ThreadingTCPServer(address, SimpleHTTPRequestHandler) as server: server.serve_forever()
HTTPServerはTCPServerを継承したクラスで、server_nameくらいしか差異がありません。
https://github.com/python/cpython/blob/v3.6.8/Lib/http/server.py#L130-L139
class HTTPServer(socketserver.TCPServer): allow_reuse_address = 1 # Seems to make sense in testing environment def server_bind(self): """Override server_bind to store the server name.""" socketserver.TCPServer.server_bind(self) host, port = self.server_address[:2] self.server_name = socket.getfqdn(host) self.server_port = port
場合によっては、この部分を引っ張ってきてもよいでしょう。
Python 3.7の場合は、ThreadingHTTPServerというクラスが追加されます。
http.server.ThreadingHTTPServer
https://github.com/python/cpython/blob/v3.7.4/Lib/http/server.py#L143-L144
どちらにしろ、スレッドの部分を扱っているのはThreadingMixInなので、TCPServerの時と同じ読み方で良さそうです。
なお、ThreadingHTTPServerの登場に伴い、これで起動するHTTPサーバーもマルチスレッドになったようです。
$ python3 -m http.server Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
https://github.com/python/cpython/blob/v3.7.4/Lib/http/server.py#L1219-L1220
つまり、Python 3.6まではシングルスレッド…。