並行処理
軽量な並行処理はWrenの重要な機能で、ファイバーを使って表します。ファイバーは、すべてのコードの実行方法を制御し、エラー処理では例外の代わりになります。
ファイバーはスレッドに少し似ていますが、協調的にスケジュールします。つまり、指示しない限り、Wrenは一つのファイバーを一時停止して別のファイバーへ切り替えません。思いがけないタイミングでのコンテキスト切り替えや、それに伴う問題を心配する必要がありません。
WrenはVM内のすべてのファイバーを管理するため、OSのスレッド資源を使わず、重いコンテキスト切り替えも必要ありません。各ファイバーに必要なのは、スタックのための少量のメモリーだけです。参照されなくなると、ほかのオブジェクトと同じようにガベージコレクションの対象になるので、自由に作成できます。
例えば、ゲーム内の各エンティティーに別々のファイバーを割り当てられるほど軽量です。Wrenは何千ものファイバーを余裕で処理できます。例えば、対話モードでWrenを実行すると、入力したコードの各行について新しいファイバーを作ります。
ファイバーの作成 #
すべてのWrenコードは、ファイバーのコンテキスト内で実行します。最初にWrenスクリプトを開始すると、メインファイバーを自動的に作成します。Fiberクラスのコンストラクターを使うと、新しいファイバーを作れます。
var fiber = Fiber.new {
System.print("This runs in a separate fiber.")
}
コンストラクターは、そのファイバーが実行するコードを含む関数を受け取ります。関数のパラメーターはゼロ個か一個で、それより多くはできません。ファイバーを作っても、直ちに実行するわけではありません。関数を包んで、起動を待つだけです。
ファイバーの呼び出し #
ファイバーを作成したら、そのcall()メソッドを呼び出して実行します。
fiber.call()
これにより、現在のファイバーを中断し、呼び出したファイバーを、本体の終わりに達するか、さらに別のファイバーへ制御を渡すまで実行します。本体の終わりに達したら、完了と見なします。
var fiber = Fiber.new {
System.print("It's alive!")
}
System.print(fiber.isDone) //> false
fiber.call() //> It's alive!
System.print(fiber.isDone) //> true
呼び出したファイバーが終了すると、それを呼び出したファイバーへ、自動的に制御を戻します。すでに完了したファイバーを呼び出そうとすると、実行時エラーになります。
yieldによる制御の返却 #
ファイバーと関数の主な違いは、ファイバーを処理の途中で中断し、後で再開できることです。別のファイバーを呼び出すのは、ファイバーを中断する一つの方法ですが、それはおおむね、一つの関数が別の関数を呼ぶのと同じです。
興味深いのは、ファイバーがyieldするときです。yieldしたファイバーは、それを実行したファイバーへ制御を戻しますが、現在の位置を覚えています。次にそのファイバーを呼び出すと、中断した位置から、そのまま実行を続けます。
Fiberの静的yield()メソッドを呼び出すと、ファイバーをyieldさせられます。
var fiber = Fiber.new {
System.print("Before yield")
Fiber.yield()
System.print("Resumed")
}
System.print("Before call") //> Before call
fiber.call() //> Before yield
System.print("Calling again") //> Calling again
fiber.call() //> Resumed
System.print("All done") //> All done
このプログラムは並行処理を使っていても、決定的である点に注意してください。何を行うかを正確に推論でき、コードでロシアンルーレットをするスレッドスケジューラーに振り回されません。
値を渡す #
ファイバーの呼び出しとyieldは制御を渡すために使いますが、データも渡せます。ファイバーを呼び出す際、必要に応じて値を渡せます。
パラメーターを取る関数でファイバーを作成した場合、call()を通じて値を渡せます。
var fiber = Fiber.new {|param|
System.print(param)
}
fiber.call("Here you go") //> Here you go
ファイバーがyieldして再開を待っている場合、callに渡した値は、再開時にyield()呼び出しの戻り値になります。
var fiber = Fiber.new {|param|
System.print(param)
var result = Fiber.yield()
System.print(result)
}
fiber.call("First") //> First
fiber.call("Second") //> Second
ファイバーは、yieldするときに値を返すこともできます。yield()に引数を渡すと、ファイバーの呼び出しに使ったcall()の戻り値になります。
var fiber = Fiber.new {
Fiber.yield("Reply")
}
System.print(fiber.call()) //> Reply
関数呼び出しが値を返すのに少し似ていますが、ファイバーはyieldのたびに一つずつ、値の列全体を返せます。
完全なコルーチン #
ここまでの機能は、ジェネレーターを持つPythonやC#などの言語でできることによく似ています。ジェネレーターでは、中断して再開できる関数呼び出しを定義できます。使う側からは、反復処理できるシーケンスのように見えます。
Wrenのファイバーにもそれができますが、さらに多くのことができます。Luaと同様、完全なコルーチンであり、コールスタックのどこからでも中断できます。ファイバーの作成に使った関数がメソッドを呼び、そのメソッドが別のメソッドを呼び、それが三つ目のメソッドを呼んで、最後にyieldを呼ぶこともできます。そのとき、それらすべてのメソッド呼び出し、つまりコールスタック全体を中断します。例えば、次のようになります。
var fiber = Fiber.new {
(1..10).each {|i|
Fiber.yield(i)
}
}
ここでは、each()メソッドに渡す関数の中からyield()を呼び出しています。Wrenでは、内側のyield()呼び出しが、each()の呼び出しと、そのコールバックとして渡した関数を中断するので、問題なく動作します。
制御の移譲 #
ファイバーには、もう一つの機能があります。call()を使ってファイバーを実行すると、yield時にどのファイバーへ戻るかを記録します。これにより、ファイバー呼び出しの連鎖を作れます。呼び出したファイバーがすべてyieldまたは終了すると、最終的にメインファイバーまで戻ります。
通常は、この動作を望むでしょう。しかし、ファイバーのプールを管理する独自のスケジューラーを書くような低水準の処理では、ファイバーを明示的にスタックとして扱いたくない場合もあります。
そのようなまれな場合のために、ファイバーにはtransfer()メソッドもあります。これは移譲先のファイバーへ実行を切り替え、移譲元のファイバーを「忘れます」。移譲元は、その時点の状態のまま中断します。明示的にそちらへ再び制御を移譲するか、呼び出すことで再開できます。そうしなければ、最後の移譲先のファイバーが戻ったときに、実行が停止します。
call()とyield()が関数の呼び出しと復帰に似ているのに対し、transfer()は構造化されていないgotoのように動作します。対等な複数のファイバーの間で、自由に制御を切り替えられます。
エラー処理 →← クラス