

Subject: [ruby-dev:30827] Supporting Fiber

From: SASADA Koichi <ko1@ d . t

Date: Mon, 28 May 2007 05:01:17 +0900

ささだです。 Continuation のついでに Fiber を入れました。金曜日には出来てたんです が、もろもろあってコミットが遅くなってしまいました。 というわけで、皆様にいくつかご相談です。おもに、名前と機能の話です。広 くご意見を頂けると幸いです。 開発した「Fiber」の概要： Fiber はノンプリエンプティブなスレッドで、MicroThreadと言われたりしま す。Coroutine と言っても良いかもしれません。ググればたくさん情報は出てく ると思います。 従来のスレッドでのコンテキスト切り替えのタイミングは、おもに次の３つで した。 (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