Doge log

Abby CTO 雑賀 力王のオフィシャルサイトです

greenletのススメ

最近、non-bloking I/O+強調スレッド+prefork(multiprocessing)なサーバフレームワーク的なものを
作ろうかと試行錯誤してる。
(aioはその次)
でこいつを実現する上で重要なgreenletの解説があんましないので書いてみる。

greenletとは

stackless pythonのようにマイクロスレッド(この言い方好きじゃないけど)が使えるようになるライブラリ。
(実際にはstackコピーと復元を行って継続をサポートする。それを応用してスレッドを実現)
こいつはモジュールとして提供されるのでstacklessのように処理系を丸ごと入れ替えることなく使用できる。
yieldを使ったマイクロスレッドとは異なりどこからでもスイッチ可能。
(ネストの影響を受けない)
ネイティブスレッドではないのでスイッチなどの処理は自前で行う必要がある。
(協調スレッド)

メリット

  1. ネイティブスレッドよりも軽量
  2. 協調スレッドなのでデータを壊すような事はない(ロックなども考えなくてよい)
  3. スイッチ処理のタイミングが任意にできる(タイムスライスではなく自由)

デメリット

  1. スイッチ処理、スケジュールを自分で書かないといけない
  2. ブロックするコードを書くと全体が止まる

デメリットにもあるようにI/O処理でブロックすると全体が止まってしまう。
この問題を回避するためにnon-blocking I/Oとセットで使用するのが定番。

ネイティブスレッドとの結合

greenletはスレッドごとにtree構造を持つので異なるスレッドで生成したgreenletにはスイッチできない。

サンプル

import greenlet

def cb1(x, y): 
    ret = gr2.switch(x+y)
    print(ret)

def cb2(z):
    print(z)
    gr1.switch("finish")

gr1 = greenlet.greenlet(cb1)
gr2 = greenlet.greenlet(cb2)
gr1.switch('Hello', 'World')

greenletには実行する関数を渡す。
処理を切り替えるにはswitchを呼ぶ。
実にシンプル。
実行すると以下のようになる。

HelloWorld
finish

    #gr1に切り替える際に値を渡す
    gr1.switch("finish")

元のgreenletにスイッチする際に引数を渡すとその値が返ってくる。

greenletは親子関係を持つことができる。
子が終了すると親が呼ばれる。

import greenlet

def cb1(x):
    print('finish')

def cb2(y):
    print(y)

gr1 = greenlet.greenlet(cb1)
gr2 = greenlet.greenlet(cb2)
gr2.parent = gr1 
gr2.switch('A')

実行結果

A
finish

runを呼ぶとスイッチせずそのまま実行する。
(gr1が呼ばれない)

import greenlet

def cb1(x):
    print('finish')

def cb2(y):
    print(y)

gr1 = greenlet.greenlet(cb1)
gr2 = greenlet.greenlet(cb2)
gr2.parent = gr1 
gr2.run('B')

non-blockingI/Oのイベントとの併用ではコードがシンプルになる。

(全部書くと長いので抽象的なコードで)

def trampoline_read(fd):
    
    # 現在実行中のgreenletを取得
    self = greenlet.getcurrent()
    
    #イベントが発生したら呼ばれるコールバック
    def cb(_fileno):
        #監視対象から外す
        ev.remove_reader(fd)
        #呼び出し元にスイッチ
        self.switch(fd) 
    
    #監視対象に追加
    ev.add_reader(fd, cb)
    
    #監視する側など他にスイッチ
    return ev.switch()

この例では以下のような流れになる

  1. 現在のgreenletを参照
  2. イベント監視対象に登録する
  3. イベントを監視するループのあるgreenletにスイッチ
  4. イベント発生
  5. コールバックが呼ばれ元のgreenletにスイッチして復帰

実際に使用する際のコード

class Reader(object):
    ...
    def read(self):
        trampoline_read(self.fd)
        return self.fd.read()
    ...
            
def test(reader):
    ....
    #このreadの中でtrampolineを呼び、この処理をここで一時中断する
    ret = reader.read()
    #この下の処理はイベント発生後に実行
    ....
    

test関数は見た目普通のブロックするようなコードに見える。
readでI/O処理が発生するがnon-bloking I/Oを使用することでブロックを回避する。
監視対象にして読み取れる状態になると戻ってきて続きを実行する。
その間はその他の処理を走らせることができる。

non-blockingなコードだとコールバックの嵐になり複雑化、全体の把握の困難などが発生する。
(twistedだとこの辺はDeferredの嵐になる)
だが協調スレッドとの併用で上記のように継続敵なコードでシンプルに記述できる。
(スイッチが自由なメリットはここにある。)


non-blocking I/Oはtwistedが定番なんだけど協調スレッドを併用することで複雑な処理もシンプルに記述できるようになる。
なのでeventletの方がよりモダン(?)だと思う。