オブジェクトをそのままで送受信するPipe
IPCの中でもPipeはTCPとかのネットワーク系の基礎になったもので、結構使いやすくて個人的に好きな機能です。
特にスレッドを使うと、スレッド間通信用に多用します。
自分でリングバッファとか作るのも1つの手なのですが、自前でこういうの作るとブロッキングが出来なくsleepとかいれざるを得ないのでちょっと厄介なので特別な理由がない場合使いません。
さて、これを今回Rubyで使おうと思ったのですがRubyの場合IO#pipeで作りwrite/readを使うので文字列でしか通信できないのがネック。
送受信したいのはあくまでもRubyのオブジェクト。Cだと、どうせ同一プロセス内なので型を統一してポインタをwrite、readしたらキャストすればOKなのですがこの技が使えない。
http://docs.ruby-lang.org/ja/2.1.0/class/IO.html#S_PIPE
http://docs.ruby-lang.org/ja/2.1.0/class/IO.html#I_WRITE
http://docs.ruby-lang.org/ja/2.1.0/class/IO.html#I_READ
諦めるのも嫌だし仕方がないので、悩んだのですがMarshalを使うことにして動作するのを確認しました。
http://docs.ruby-lang.org/ja/2.1.0/class/Marshal.html
class ObjctPipe def initialize pipe = IO.pipe @rp = pipe[0] @wp = pipe[1] end def write(obj) objs = Marshal.dump(obj) len = objs.length lens = [len].pack('L!') @wp.syswrite(lens) @wp.syswrite(objs) end def read lens = @rp.sysread(8) len = lens.unpack('L!')[0] objs = @rp.sysread(len) obj = Marshal.load(objs) return obj end end # 使い方はこうなります pipe = ObjctPipe.new # read側はスレッドで行う Thread.new do while true obj = pipe.read end end # write側 while true # データを作成しておく pipe.write("なにかobject") end
いくつかポイント
- write側スレッドとread側スレッドで同じ'pipe'オブジェクトを使いますが、Mutexは使っていない
- これは、実質同じデータにアクセスしないのでスレッド特有の問題は発生しないからです
- 送信するobjectをMarshalしておく
- 当然Marshalの制限がかかります
- objectを送信する前にオブジェクトサイズを送る
- サイズはpackで'unsigned long'にしておき、'オブジェクトサイズ'自体の大きさを固定しておく
- これはread時のバッファやブロッキング動作が絡むからです
- write/readではなくsyswrite/sysreadを使う
- これは文字コードが絡んできてデータによってはwriteでエラーになるから
Rubyってobj.__id__でオブジェクトIDは作れるのですが、IDからオブジェクトの復活方法が分からないのでこの方法を取りました。
Marshalやpack/unpackが入るのでCに比べて無駄が多いのですが、まあ許容範囲内でしょう。
意外と便利に使えています。