Gevent
稲田 直哉
KLab株式会社
ソース
http://github.com/methane/pyconjp2012-gevent-slide
スライド
http://methane.github.com/pyconjp2012-gevent-slide
簡単かつ効率のいい IO多重化
複数のIO処理を並行に扱うこと.
スレッドを止めてIOを待つ
スレッドを複数使うことで多重化
IOを待たないでエラーを返す.
複数のIOをまとめて待つ(selectなど)ことで多重化
実行可能になったIOに対応する処理を実行する イベントドリブン プログラム.
1クライアントとしか通信できない
import socket def echo(sock): try: while True: data = sock.recv(1024) # 受信できるまでブロック if not data: break sock.sendall(data) # 送信できるまでブロック finally: sock.close() def serve(addr): sock = socket.socket() sock.bind(addr); sock.listen(50) while True: conn, _ = sock.accept() # 接続されるまでブロック echo(conn) # 終わるまで帰ってこない serve(('0.0.0.0', 4000))
並行処理したい関数をスレッドで包むだけ
import socket, threading def echo(sock): try: while True: data = sock.recv(1024) # 受信できるまでブロック if not data: break sock.sendall(data) # 送信できるまでブロック finally: sock.close() def serve(addr): sock = socket.socket() sock.bind(addr); sock.listen(50) while True: conn, _ = sock.accept() threading.Thread(target=echo, args=(conn,)).start() serve(('0.0.0.0', 4000))
かなり面倒
#... def on_readable(self): while True: conn, _ = self.sock.accept() EchoHandler(conn) #... def on_readable(self): try: data = self.sock.recv(4096) if not data: self.close() return self.buf.append(data) finally: self._update() #...
基本はコールバック使ったイベントドリブンのまま。
from tornado import ioloop, iostream from tornado.netutil import TCPServer class EchoServer(TCPServer): def handle_stream(self, stream, addr): stream.read_until_close( lambda _: stream.close(), # 切断時コールバック stream.write, # データ受信コールバック ) def serve(addr): server = EchoServer() server.listen(addr[1], addr[0]) ioloop.IOLoop.instance().start() serve(('', 4000))
echoサーバー
Gevent vs Threading
Gevent vs Tornado
from gevent.server import StreamServer def handler(sock, addr): try: while 1: buf = sock.recv(4096) if not buf: return sock.sendall(buf) finally: sock.close() def serve(addr): server = StreamServer(addr, handler, backlog=1024) server.serve_forever() serve(('', 4000))
import gevent.monkey; gevent.monkey.patch_all() import socket, threading def echo(sock): try: while True: data = sock.recv(1024) # 受信できるまでブロック if not data: break sock.sendall(data) # 送信できるまでブロック finally: sock.close() def serve(addr): sock = socket.socket() sock.bind(addr); sock.listen(50) while True: conn, _ = sock.accept() threading.Thread(target=echo, args=(conn,)).start() serve(('0.0.0.0', 4000))
echo サーバーに 1000接続から1000回ずつ、
計100万回のメッセージを送受信.
threading: 34MB
gevent: 26MB
tornado: 12MB
select: 6.1MB
threading: 3.9GB
gevent: 41MB
tornado: 27MB
select: 21MB
32bit 環境では2GBしかメモリ空間がないので致命的(C10K問題).
64bit 環境では無視できる。
threading:
43sec
gevent: 53sec
tornado: 43sec
select: 25sec
2000接続から50回ずつ、計10万回リクエスト
send の手前で負荷をかけてみる
def stress(): # 18.6 ms def rec(n): if n: return rec(n-1) for i in xrange(100): rec(100)
Gevent | Threading | |
---|---|---|
RSS | 46.1MB | 210.5MB |
VSS | 46.5MB | 7.9GB |
time | 3m20sec | 10m55sec |
スレッドのオーバーヘッド:
深い関数呼び出し => メモリ使用量が増える
CPUを使う処理がたくさん並行する
=> 実行時間が増える
たいていスレッドで十分
ワクワクするから Gevent を使うというのはアリ :-)
マルチコア・マルチスレッド・高負荷のとき
スレッドのオーバーヘッドが大きい(GIL)場合は、 Gevent の方が安定した性能が出る.
メモリを節約したい場面でも有効
イベントドリブンだと処理が細切れになりがち.
def spamegg(a): b = spam() return egg(a, b)
class SpameHamEgg(object): def bake(self, a, callback): self.a = a self.callback = callback spam(callback=self.on_spam) def on_spam(self, b): egg(self.a, b, callback=self.callback)
ジェネレータを使ったコルーチン.
from tornado import gen @gen.engine def spamegg(a): b = yeild spam() return egg(a, b)
callback が呼ばれるのは try-catch ブロックの外.
イベントドリブンでは try-catch に代わる仕組みが必要。
def spamegg(): try: a = spam() return egg(a) except Exception as e: log.error(e) return None
import contextlib @contextlib.contextmanager def log_error(): try: yield except Exception as e: log.error(e) def spamegg(): with StackContext(log_error): spam(callback=egg)
多くのライブラリがモンキーパッチで動く.
後から Gevent に対応するのも容易.
最初から Tornado 用に設計されてないと対応が難しい.
gevent は monkey patch だけで動く
Tornado に対応させるために Motor が作られた。 (Gevent のような仕組みをTornadoで実現)
Tornado, Twisted, node.js はそれぞれイベントドリブンプログラミングのためのフレームワークとしてとてもおもしろい。
パフォーマンスについても、 Tornado や Twisted の方が若干軽く、しかも PyPy に対応できる。
Gevent は 今までと同じプログラムの書き方ができ、
既存のライブラリを対応させるのも容易
明示的に切り替えが必要な軽量スレッド(コルーチン)
import greenlet def f1(): print 'f1', 1 g2.switch() print 'f1', 3 g2.switch() print 'f1', 5 def f2(): print 'f2', 2 g1.switch() print 'f2', 4 g1.switch() g1 = greenlet.greenlet(f1) g2 = greenlet.greenlet(f2) g1.switch()
実行結果
f1 1
f2 2
f1 3
f2 4
f1 5
勝手にスイッチしない(スレッドセーフに書きやすい)
ブロックするシステムコールを実行すると、ほかのスレッドに切り替えることができない
libev ラッパー
イベントループを抽象化する.
import gevent.core import time loop = gevent.core.loop() def callback(): print time.time() # 繰り返しタイマーイベント timer = loop.timer(1.0, 1.0) timer.start(callback) loop.run()
実行結果:
1347446334.99
1347446335.99
1347446336.99
1347446337.99
...
イベントループと greenlet を繋げる greenlet
import gevent.core, greenlet, time # hub = gevent.get_hub() の簡易版 loop = gevent.core.loop() hub = greenlet.greenlet(loop.run) # gevent.sleep() の簡易版 def sleep(seconds): timer = loop.timer(seconds) # コールバックで現在の greenlet に switch させる timer.start(greenlet.getcurrent().switch) # hub に switch してイベントループにもどる hub.switch() def sleeper(): for _ in range(4): print time.time() # ブロックする関数として実行可能 sleep(1) sleeper() hub.switch()
実行結果:
1347448193.7
1347448194.7
1347448195.71
1347448196.71
gevent.core
の PyPy 向け実装が今朝投稿された。
PyPy 最新開発版の FFI を使っているので 1.9 じゃ動かない
http://sdiehl.github.com/gevent-tutorial
http://methane.github.com/gevent-tutorial-ja
https://github.com/SiteSupport/gevent
http://gevent.org/