Python の multiprocessing package

って Ruby のパパが言ってたので、どんなものかなと慣れない Python を触ってみました。実際コード読んだりしたのは 30分くらいで書いてるのですごい適当です。あまり鵜呑みにしないでください。

どうやら processing は Python 2.6 からは標準添付に取り込まれて multiprocessing というパッケージに改名したようです。使いかたは以下の読めばだいたい感じがわかると思います。ドキュメントがしっかりしていますねー。

http://docs.python.org/library/multiprocessing.html

まあ子プロセス起動して関数呼ぶだけならこんなです。

def f(str):
  print(str);

pr = Process(target=f, args=("hello",));
pr.start()
pr.join()

いわゆる戻り値は取れないようです。データをやりとりしようと思ったら Queue とか Pipe といった専用のオブジェクトを用意してそれを渡しすようにするようです。同期は Lock を使います。あとこれはよく調べていませんが Manager というのを使うと、親プロセスがサーバとして動作してコールバックでいくつかの対応したオブジェクトは子プロセスから操作できるように自動で Proxy オブジェクトを作って渡してくれるみたいです。
なお値のやりとりには pickle という Ruby でいう Marshal に相当するモジュールで serialize して送受信するそうです。

で、Ruby には dRuby(drb) という超すごいライブラリが標準添付されていまして*1、こういう仕組みは既にあるし、主観的にはメソッド呼び出しが透過的に実行できるなど drb のほうが優れている点が多いと思います。

けど multiprocessing のお手軽さというのもあって、それは Process に関数渡すだけでよくて、背後では pipe とかで繋いでやりとりしてるわけですがそういうのは見えなくされてる。いやそんな低レベルな操作は drb でも見えなくされていますが、若干の準備は必要です。ここでちょっと multiprocessing の実装をみてみます。

multiprocessing/process.py に Process クラスの定義があって、run というのが子プロセスが関数を呼びだすところで start() が親プロセスで起動をトリガするところなわけです。

    def run(self):
        ''' 
        Method to be run in sub-process; can be overridden in sub-class
        '''
        if self._target:
            self._target(*self._args, **self._kwargs)

    def start(self):
        ''' 
        Start child process
        '''
        assert self._popen is None, 'cannot start a process twice'
        assert self._parent_pid == os.getpid(), \
               'can only start a process object created by current process'
        assert not _current_process._daemonic, \
               'daemonic processes are not allowed to have children'
        _cleanup()
        if self._Popen is not None:
            Popen = self._Popen
        else:
            from .forking import Popen
        self._popen = Popen(self)
        _current_process._children.add(self)

結構読みやすいですね。 Python は「みんなの Python」を読んだことがある程度で初心者ですけど、それでもまあなんとか読めます。 _Popen というのは None に初期化されてるのであえて変更していなければ forking モジュール(?) の Popen を利用するみたいです。

なので次は multiprocessing/forking.py を読んでみます。まず Popen の実装は "if sys.platform != 'win32':" という感じで Windows とそれ意外(Unix)でざっくりと分離されています。漢らしい。ここでは Unix 版の class Popen から抜粋してきます。Popen の肝は初期化時です。

    class Popen(object):

        def __init__(self, process_obj):
            sys.stdout.flush()
            sys.stderr.flush()
            self.returncode = None

            self.pid = os.fork()
            if self.pid == 0:
                if 'random' in sys.modules:
                    import random
                    random.seed()
                code = process_obj._bootstrap()
                sys.stdout.flush()
                sys.stderr.flush()
                os._exit(code)

わかりやすいですね。 os.fork() によって fork(2) しています。そして子プロセス側では乱数のseedを再設定してから process_obj の _bootstrap() を呼んでます。 process_obj は Process のインスタンスです。また multiprocessing/process.py に戻ります。

前準備や例外処理などでちょっと長いので本質的だと思う部分だけ抜粋します。

    def _bootstrap(self):
        try:
            try:
                os.close(sys.stdin.fileno())
            except (OSError, ValueError):
                pass
            try:
                self.run()
                exitcode = 0
            finally:
                util._exit_function()

標準入力は閉じてるんですね。
まあそれはともかく、結局のところ Process の run メソッドが呼ばれています。run は最初にみたように Process() に渡された関数と引数を使って呼び出すだけですね。

なので Ruby でもの凄く簡略化して書くとしたらこんな感じですね。Ruby なら関数渡すかわりにブロック渡しになるでしょうけど、Kernel#fork はもともとブロック渡しに対応しています。

str = "hello"
pid = fork do
  puts str
end

Process.waitpid(pid)

親プロセスで DRb.start_service しておいて、 TupleSpace あたりを利用してデータを受け渡しするというのを wrap してあげればなにか使いやすいインタフェースを提供するのはできそうです。……というか個人的にはそのままで充分使いやすいと思います。

しかしここでちょっと気になることがあるわけです。なにかというと、Ruby で Kernel#fork で子プロセスを作って Kernel#exec でプロセスイメージを上書きしないでそのまま動き続けるのはちゃんと動かないかもしれない。ちゃんと最新の情報を調べずに記憶で書いてしまいますけど、*BSD では pthread でスレッドを作ったプロセスが fork すると子プロセスで新しいスレッドが起動できないらしい。CRuby はタイマースレッドというのを起動していて、fork する時には一旦これを止めて fork してから親子の両方で再起動しますから、BSD では fork できないらしい。本当かなぁちょっと自信なくなってきた。このあたりは嘘かもしれないです。
まあでも問題はそれだけじゃなくて、元々マルチスレッドの親プロセスから fork すると、子プロセスではメインスレッド以外は即座に停止されるのですが、この時 Mutex とか Monitor のロックを取りっぱなしで停止されてしまって、Mutex は今だと pthread_mutex になってるのでもしかしたら大丈夫かもしれませんが Monitor とかは不正な状態になっててデッドロックしてしまったりする。まあマルチスレッド使うプロセスから fork するなということですけど。等々、fork 使うな spawn*2使えというのが最近のジャスティスです。しかし Proc オブジェクトは現状 serialize できませんから子プロセスに実行すべきメソッドなり手続きなりを渡すには適切に require するなりなんなりしないといけないわけです。なんかだんだん大変なような気がしてきましたね。

疲れてきたので今日はこのくらいにします。特に結論っぽいものはないですがまとめると。

  • multiprocessing なかなかお手軽
  • drb は至高
  • だが CRuby の fork して続行には不安がある。しかし spawn するとなると子プロセスは独立したコマンドになるので、ブロックを渡してちょっとこれだけ実行して、というのができない
  • Binding を持たないで Marshal.dump 可能な Proc オブジェクトみたいなものができればいいんじゃないかなぁ。とここで ytl の出番とか。

勢いだけで書いたので間違いが多々あると思いますので変なところはツッコミお願いします。

[追記]そういえば大事なことを忘れてました。 dRuby については「dRubyによる分散・Webプログラミング」を読むのがいいと思います。

dRubyによる分散・Webプログラミング

dRubyによる分散・Webプログラミング

みんなでこの本を買って増刷させて「まだ初版買えます」という咳さんの定番ネタを使えなくしてしまいましょう。[/追記]

*1:Ruby のすばらしさの 6割くらいは drb があるからだとわたしは思います

*2:fork+exec を一度に実行してくれる