ささだです。

 Continuation のついでに Fiber を入れました。金曜日には出来てたんです
が、もろもろあってコミットが遅くなってしまいました。

 というわけで、皆様にいくつかご相談です。おもに、名前と機能の話です。広
くご意見を頂けると幸いです。



開発した「Fiber」の概要:

 Fiber はノンプリエンプティブなスレッドで、MicroThreadと言われたりしま
す。Coroutine と言っても良いかもしれません。ググればたくさん情報は出てく
ると思います。

 従来のスレッドでのコンテキスト切り替えのタイミングは、おもに次の3つで
した。

 (1) Thread.pass を使った時
 (2) IO待ちになったとき
 (3) 一定時間かかったとき

 Fiber は、どの場合でも切り替わりません。明示的に「どのファイバへ処理を
移すのか」を示さなければなりません。そのため、複雑な同期処理を行わない並
行処理が可能になります。また、Fiber の生成は軽量なので、数万 Fiber の生
成が数秒で終わります。多数のエージェントを生成して行うようなシミュレー
ションの用途には向くかもしれません(ネイティブスレッドを利用すると、OS
のスレッド生成限界に縛られます。32bit Linux だと、頑張っても数千スレッド
が限界になります)。

 ただし、IO待ちになっても処理は切り替わらない(ブロックする)ため、ネッ
トワークプログラミングにはおそらく利用できないでしょう。継続ベースのなん
とかかんとか、だったら利用可能かもしれませんが。

 なお、Fiber という「名前」は Windows の API に存在します。これが由来?
CreateFiber() により、Fiber を作成し、SwitchToFiber() で、指定のファイバ
に処理を移します。元の Fiber(暗黙に生成される、スレッドにひとつ付属する
ファイバ)へ戻らないでファイバの実行が終了した場合、自分の環境ではプログ
ラムが落ちました。



 現在作ってみたFiberの使い方は、こんな感じの仕様です。ちなみに、実装
は、要するに matzruby の greenthread から「preemptive なスレッド切り替
え、その他同期API」を引いたものになります。


* 新しいファイバを作ってみる

  f = Fiber.new{
    :ok
  }

* ファイバ f へ処理を移す

  f.pass

* ファイバへ、値付きで処理を移す

  f.pass(value)

* 複数のファイバで処理を移動しあう

  f2 = nil
  f1 = Fiber.new{f2.pass; f2.pass}
  f2 = Fiber.new{f1.pass; f1.pass}
  f1.pass

* 現在動いている Fiber を知る

  Fiber.current # Thread.current と同じ発想です。

* 最後に処理を移した Fiber を知る

  Fiber.prev もしくは Fiber#prev

  # 利用例
  f0 = Fiber.current
  f1 = Fiber.new{
    p(Fiber.prev == f0) #=> true
  }
  f1.pass

* 暗黙に生成される Fiber を知る

 スレッドが生成されると、暗黙のうちにひとつ Fiber が生成されることにな
ります。それを、ここでは便宜上 root Fiber と言うことにします。実はまだ実
装していませんが、Fiber.root でとれるといいかな、と思っています。が、
取って何するのかはよくわかりません。

 ちなみに、root Fiber が終了した時点で関連づけられているスレッドは終了
することになります。

* Fiber 終了時の処理の流れ

 いろいろ考えて、次のようにするようにしました。

  (a) まず、Fiber.prev に pass
  (b) ただし、Fiber.prev がすでに終了している場合、Fiber.root へ pass

 Fiber に渡すブロックの評価値は、pass で渡ることになります。

 たとえば、

  p Fiber.new{
    :ok
  }.pass #=> :ok

となります。



ご相談:

(1) そもそも、Ruby にこんな機能を加えていいですか?

 とりあえず、Generator は大変書きやすかったです。

(2) Fiber という名前は適当でしょうか?

 Coroutine という名前のほうがいいのかなぁ、という気もしています。Fiber
だと、Thread に引きずられて誤解が生じる可能性があるかもしれません。
ちょっとわかりません。

(3) API 名は適当でしょうか?

 というか、適当じゃないと思います。Thread#pass との連想で Fiber#pass と
しましたが、「機能の連想で、スレッドに引きずられる」、「fib#pass で fib
に処理が移るとは思いづらい」などの話があると思います。

 また、最近の言語にあるような suspend/resume に限定した API にするのも
手かもしれません(semi-coroutine)。機能が限定されるので、使いやすいから
です。ただ、現在の Fiber のほうが自由度は高い(callee/caller の関係は、
ないものとして利用可能)ので、semi-coroutine はライブラリ実装にする、と
いうのも手かと思います。


 で、いろいろメソッド名を考えてみました。


class Fiber
  alias start pass    # 初回以外でも使えていいものか (*)
  alias restart pass  # 初回に使えていいものか
  alias resume pass   # suspend してないのに使えていいものか
  alias kick pass
  alias call pass     # call だと、対等の関係じゃないっぽい
  alias goto pass     # fib.goto は変だ
  alias yield pass    # 機能紛らわしくない?
  alias transit pass

  def suspend *args   # 呼び出し元へ戻る。semi-coroutine / 確かに便利
    Fiber.prev.pass *args
  end
end

(*) しかし、初回かどうかを区別するためにフラグを設けるのも無駄っぽい。



 もう、上記の名前を全部サポートする、とかでもいいのかもしれません。うー
ん、やっぱり駄目かな。よくわかりません。どうしましょう。


(4) もっとほかの API

 他にも必要な API って要るでしょうか。



* 例

 よくありそうな、Producer/Consumer の例です(上記の yield の alias を
使っています)。


# producer/consumer sample

root = Fiber.current
consumer = nil

producer = Fiber.new{
  10000.times{|e|
    consumer.pass e
  }
  consumer.pass nil
}

consumer = Fiber.new{
  while v = producer.yield
    p v
  end
}

producer.pass


 Fiber で Generator を作った例です(上記の suspend/resume を使っていま
す)。Thread(や callcc)を使うより、ずいぶんすっきりしています。


# Generator sample

class Generator
  def initialize enum = nil, &block
    @finished = false
    @index = 0
    @enum = enum
    @block = block

    @fib = if block_given?
      Fiber.new{
        yield self
        @finished = true
      }
    else
      Fiber.new{
        enum.each{|e|
          @e = e
          @fib.suspend
        }
        @finished = true
      }
    end
    @fib.start
  end

  def next?
    !@finished
  end

  def next
    raise "No element remained" if @finished
    ret = @e
    @index += 1
    @fib.resume
    ret
  end

  def end?
    !self.next
  end

  def yield value
    @e = value
    @fib.suspend
  end

  def current
    @e
  end

  def index
    @index
  end

  alias pos index

  def rewind
    initialize(@enum, &@block) if @index.nonzero?
    self
  end
end

def show g
  while g.next?
    p g.next
  end
end

show Generator.new([:a, :b, :c])
show Generator.new(1..3)
show Generator.new{|g|
  g.yield 10
  g.yield 20
  g.yield 30
}


-- 
// SASADA Koichi at atdot dot net