著作一覧 |
というわけで(昨日の続き)、DDJJ 1996年10月号の『非同期設計パターン』だが、.NET Frameworkではそれらしきものが採用されているものの、そんなに広く使われているわけでもなさそうだ。
IOUパターンは、非同期IOを同期IOモデルのように、オブジェクトの利用者に見せかけるためのパターンで、非同期IOの結果をまるで同期IOのように、呼び出し側に返す。しかし、同期IOではなく、すぐに返す。
そのため、呼び出し側はお話にならないエラーはすぐに検出できる(これは同期IOでも同様。たとえばクローズ済みIOに対してメソッドを呼んだ場合)。
そうではない、たぶん、実行されるであろうIOについても、すぐに戻る。ただし、結果として返されるオブジェクトは、同期IOの場合と異なり、IOUオブジェクトだ。このオブジェクトは、筆者のAllan Vermeulenによって以下のように定義されている。訳は上林靖氏。
IOUとは、サプライヤ(呼び出された関数)からのアグリーメントであり、約束したオブジェクトを提供することによって最終的に閉じられる。いったんIOUが閉じられてしまえば、サプライヤが提供したオブジェクトでIOUを償還すればよいのである。それまでの間プログラムコードは、他の有用な作業を続けることができるのだ。
IOUの概念は、2つの理由により有効である。まず単純であること。IOUをリターンする関数の呼び出しは、そうでない関数の呼びだしと全く同じに書くことができる。第2にIOUは、サプライヤが使用する非同期機構(もしあればだが)から完全に独立していることである。
2番目の有効性については、IAsyncResultがまあ証明していると言える。
以下に、Rubyで実装したIOUの例を示す。ここではIOUはHTTP 1.1の送信と受信をカプセル化する。チャンクの処理はいい加減だがやっつけ仕事の意味のハックなのでしょうがない。
#!/usr/local/bin/ruby -Ku # coding: utf-8 require 'socket' class AsyncSocket < TCPSocket class IOU def initialize(socket, queue, data = nil) @socket = socket @queue = queue @trns = data @complete = nil @callback = nil queue << self do_io end ## # true if this IOU was closed # def close? @complete ? true : false end ## # wait until redeem is ready (IOU was closed) # def stand_by Thread.pass until @complete end ## # return the data or waiting it # def redeem stand_by @complete end ## # register user method called after closing this IOU # def add_callback(&f) p "add_callback(#{f})" if $DEBUG if @complete f.call(@complete) else @callback = f end end def inspect "#<IOU:#{hash}, @complete=#{@complete}, @callback=#{@callback}>" end protected def do_io return unless @queue.empty? || @queue[0] == self Thread.start do p "start thread by #{self}" if $DEBUG begin if @trns @complete = @socket.write(@trns) else r = '' begin r << @socket.read(1) end until r[-4..-1] == "\r\n\r\n" m = r.match(/^content-length\s*:\s*(\d+)\s*$/i) if m r << @socket.read(m[1].strip.to_i) else r << read_chunk end @complete = r end rescue @complete = $! end @queue.shift if @queue.length > 0 @queue[0].do_io end @callback.call(@complete) if @callback p "exit thread by #{self}, current queue = #{@queue}" if $DEBUG end end def read_chunk r = '' loop do sz = '' begin sz << @socket.read(1) end until sz[-2..-1] == "\r\n" p "chunk #{sz}" if $DEBUG r << sz len = sz[0..-3].to_i(16) break if len == 0 r << @socket.read(len) begin r << @socket.read(1) end until r[-2..-1] == "\r\n" end r end end def initialize(host, service) super(host, service) @queue = [] end def async_read IOU.new(self, @queue) end def async_write(s) IOU.new(self, @queue, s) end end if $0 == __FILE__ sock = AsyncSocket.new("www.google.com", 80) ious = [] ['iou', 'java', 'async'].each do |word| puts "search #{word}" iou = sock.async_write("GET /search?q=#{word} HTTP/1.1\r\nHost: www.google.com\r\n\r\n") iou.add_callback do |result| puts "wrote #{result} bytes" end ious << iou iou = sock.async_read iou.add_callback do |result| printf "%.80s\n", result end ious << iou end =begin すべてのIOUの完了を待つ場合 loop do break if ious.map {|iou| iou.close? }.uniq == [true] Thread.pass end =end 最後のIOUの完了を待つ。この例では設定したasync_callbackも実行される printf "%.80s\n", ious.last.redeem sock.shutdown end
ここでは、見掛け上は同時に複数のクェリーをGoogleに対して実行する。ただし、実際には1つのコネクションを利用して、一問一答形式で行われる。
実行例を示す。
C:\home\test>ruby ..\doc\books\rbasic\samples\3\iou.rb search iou search java search async wrote 52 bytes HTTP/1.1 200 OK Cache-Control: private, max-age=0 Date: Sat, 28 Feb 2009 15:41 wrote 53 bytes HTTP/1.1 200 OK Cache-Control: private, max-age=0 Date: Sat, 28 Feb 2009 15: wrote 54 bytes HTTP/1.1 200 OK Cache-Control: private, max-age=0 Date: Sat, 28 Feb 2009 15: HTTP/1.1 200 OK Cache-Control: private, max-age=0 Date: Sat, 28 Feb 2009 15:
4回結果を表示しているのは、最後のIOUに対してredeemメソッドの呼び出しとasync_callbackの呼び出しが行われているからだ。通常はどちらか一方を利用することになる。
このプログラムは1.8.7、1.9.1の両方で実行できるが、1.9.1では標準出力をスレッド間でシェアしているためか、改行が他のスレッドの出力に食われて正しく処理されなかったりするようだが、そういうものなのだろう。
ここで示したIOUの実装例では、実際のIOはスレッドを利用して同期的に行っている。しかし、仮に非同期IOを利用するように変えたとしても、それは呼びだし側にとってはIOUによって隠されているため、呼び出し側のコードには影響しない。
さて、これでこのDDJJを捨てられる。(他にはそれほど興味深い記事はなくもないけど、まあないとする)
ジェズイットを見習え |
Doug LeaのFutureパターンと動機と実装がほぼ同じ感じですね。
詳細を隠す(IOUの第2の目的)という意味では似ている(実装含め)と思いますが、IOUの第1の目的は、非同期IOを同期IO並みに呼び出し側にとって単純にする(呼び出し側は非同期IOを意識する必要があるので、戻り値としてIOUを返す。Futureは単に意識させないのだと思います)で、むしろそこが肝だと思うので、僕にはそれほど似ているようには思えなかったです。