Future

マルチスレッドパターン続き、今回はFutureパターンです。
これは「戻り値の準備」と「戻り値の利用」を分けましょう、というものです。
メインスレッドは一旦プロキシを受け取っておいて、プロキシに対応する実際のデータは別スレッドで走って作りましょう、ということですね。

サンプル

Rubyで実装したFutureパターンのサンプルソースです。

module FT
  require 'thread'
  require 'monitor'

  class FutureData
    include MonitorMixin

    def initialize()
      super()
      @realdata = nil
      @ready = false
      @cond = self.new_cond
    end

    def get_content
        return "future data:#{self}" unless @ready
        @realdata.get_content
    end

    def set_content
      synchronize do
        return if @ready
        @realdata = RealData.new
        @ready = true
        @cond.signal
      end
    end
  end

  class RealData
    def initialize
      puts "#{Thread.current}:start: make real data"
      sleep(5)
      puts "#{Thread.current}:end:  make real data"
    end

    def get_content
      "real data :#{self}"
    end
  end

  class Host
    def initialize
    end

    def request
      data = FutureData.new
      Thread.new do
        data.set_content
      end
      data
    end
  end

end

呼び出し側。dataの取得は一度、それに対して三つのスレッドがget_contentを呼び出しています。

require 'future.rb'

host = FT::Host.new
data = host.request

for i in 1..3 do
  Thread.new do
    loop do
      puts "get #{data.get_content}:#{Thread.current}"
      sleep(2)
    end
  end
end

sleep(7)

結果。

#<Thread:0xb7f59224>:start: make real data
get future data:#<FT::FutureData:0xb7f5929c>:#<Thread:0xb7f590bc>
get future data:#<FT::FutureData:0xb7f5929c>:#<Thread:0xb7f58ff4>
get future data:#<FT::FutureData:0xb7f5929c>:#<Thread:0xb7f58f2c>
get future data:#<FT::FutureData:0xb7f5929c>:#<Thread:0xb7f58ff4>
get future data:#<FT::FutureData:0xb7f5929c>:#<Thread:0xb7f590bc>
get future data:#<FT::FutureData:0xb7f5929c>:#<Thread:0xb7f58f2c>
get future data:#<FT::FutureData:0xb7f5929c>:#<Thread:0xb7f58ff4>
get future data:#<FT::FutureData:0xb7f5929c>:#<Thread:0xb7f590bc>
get future data:#<FT::FutureData:0xb7f5929c>:#<Thread:0xb7f58f2c>
#<Thread:0xb7f59224>:end:  make real data
get real data :#<FT::RealData:0xb7f59120>:#<Thread:0xb7f58ff4>
get real data :#<FT::RealData:0xb7f59120>:#<Thread:0xb7f590bc>
get real data :#<FT::RealData:0xb7f59120>:#<Thread:0xb7f58f2c>

前半のget_contentではFutureDataに記載された処理が返されているのに対し、後半はRealData#get_contentが返されているのが分かります。
今回のFutureData#get_contentは「できていなかったらすぐに返す」という動作のため、synchronizeは外してあります。
ここがもし「できるまで待つ」だった場合、このメソッドをsynchronizeにしなくてはいけないでしょう。

サンプルケース RSSゲッター

折角なので、このFutureパターンを利用したプログラムをと思い、RSSをマルチスレッドで走りながら取得するプログラムを作ってみました。

module GETRSS
  require 'thread'
  require 'monitor'
  require 'rss/2.0'
  require 'stringio'

  class FutureData
    include MonitorMixin

    def initialize()
      super()
      @realdata = nil
      @ready = false
      @cond = self.new_cond
    end

    def get_content
      synchronize do
        @cond.wait_until{ @ready }
        @realdata.get_content
      end
    end

    def set_content(req)
      synchronize do
        return if @ready
        @realdata = RealData.new(req)
        @ready = true
        @cond.signal
      end
    end
  end
  class RealData
    def initialize(req)
      puts "#{Thread.current}:get [#{req}]"

      sio = StringIO.new
      begin
        open(req) do |h|
          raise "not found '#{request}'" unless h
          response = h.read
          rss = RSS::Parser.parse(response, false)
          title = rss.channel.title
          rss.items.each_with_index do |item, i|
            sio.puts "#{title}(#{i}):#{item.title}" if i < 5
          end
        end
      rescue => exc
        sio.puts "Failed:#{exc}"
      end
      sio.rewind
      @data = sio.read

      puts "#{Thread.current}:get rss...done"
    end

    def get_content
      @data
    end
  end

  class Host
    def initialize
    end

    def request(req)
      data = FutureData.new
      Thread.new do
        data.set_content(req)
      end
      data
    end
  end

end

呼び出し側。

require 'future.rb'

host = GETRSS::Host.new
requests = [
  'http://headlines.yahoo.co.jp/rss/rps_dom.xml',
  'http://news.goo.ne.jp/rss/topstories/gootop/index.rdf',
  'http://rss.rssad.jp/rss/livedoornews/topics/rss.xml'
]
list = []
requests.each do |r|
  list << host.request(r)
end

list.each do |data|
  puts data.get_content
end

結果。

<Thread:0xb7e42840>:get [http://headlines.yahoo.co.jp/rss/rps_dom.xml]
<Thread:0xb7e0f6e8>:get [http://news.goo.ne.jp/rss/topstories/gootop/index.rdf]
<Thread:0xb7e09108>:get [http://rss.rssad.jp/rss/livedoornews/topics/rss.xml]
<Thread:0xb7e0f6e8>:get rss...done
<Thread:0xb7e42840>:get rss...done
Yahoo!ニュース - 国内 - レスポンス(0):マツダレンタカー、iDとEdyでの支払いが可能に(レスポンス)5日22:10
Yahoo!ニュース - 国内 - レスポンス(1):多機能型消防車、登場…消防団に助っ人(レスポンス)4日23:10
Yahoo!ニュース - 国内 - レスポンス(2):BMWジャパン、最長5年間のメンテナンスパッケージを導入(レスポンス)31日21:10
Yahoo!ニュース - 国内 - レスポンス(3):マツダレンタカー、セラピードッグの育成・派遣に協力(レスポンス)31日20:40
Yahoo!ニュース - 国内 - レスポンス(4):【ガソリン国会】暫定税率つなぎ法案 2委員会で成立したが…(レスポンス)30日21:10
注目のニュース - gooニュース(0):中国TV、日本調査が安全お墨付き
注目のニュース - gooニュース(1):「なぜ株大幅安か疑問」官房長官
注目のニュース - gooニュース(2):橋下氏、出産・子育て支援は凍結
注目のニュース - gooニュース(3):MSとヤフーの強みは「検索以外」
注目のニュース - gooニュース(4):実現しなかったオバマ圧勝、なぜ
<Thread:0xb7e09108>:get rss...done
livedoor NEWS - 主なトピックス(0):"政治家になる為行列"と批判
livedoor NEWS - 主なトピックス(1):雅子さまに同情で日本人反発?
livedoor NEWS - 主なトピックス(2):"鼻につく"OLに嫌われる大学
livedoor NEWS - 主なトピックス(3):妻の上に座って圧死させる
livedoor NEWS - 主なトピックス(4):帝拳ボクサー 韓国男児を救う

RSSの取得ログとRSS表示がまざって表示されているところから、取得と表示がマルチスレッドで実行されているのがわかります。