概要
Ver. 4.0
予定
先物(future)
継続(continuation)
・先物
「待ち時間を無駄にしない」という意味での非同期処理では、
いったん非同期に処理したうえで、処理結果の値を使って次の処理を始めたいことが多々ある
Task の場合、ジェネリック引数付きの Task<T> を使って、
非同期処理の結果を受け取ることができる
Task<T> みたいな、
「非同期に処理して、処理が終わったら値を取りたい」
みたいなものを先物(future: 将来的に受け取る物)って読んだりする。
(実際、他の言語だと Future って名前のクラスになってたりする
.NET 4 の Task でも、プレビュー版のころは Future<T> って名前になってた)
var t = Task.Factory.StartNew(() =>
{
Thread.Sleep(3000);
return 10;
});
Console.WriteLine(t.Result);
みたいなコード書くと、
t.Result の時点でタスク終了待ちに入って、3秒後にタスクの戻り値(この場合10)が取れる。
(スレッド単位のシーケンスがどうなってるか絵にしたい)
main 別スレッド
|
|-------→|
| |
| |
| |
|←-------|この間、メイン スレッド止まる
|
・継続
ただ、↑ みたいに3秒待ってしまうと非同期の意味ない。
実際には、「その処理終わったら引き続きこういう処理して」というようなデリゲートを渡す。
Task.Factory.StartNew(() =>
{
Thread.Sleep(3000);
return 10;
})
.ContinueWith(t =>
{
Console.WriteLine(t.Result);
});
こういう、「引き続き処理して」ってのを継続(continuation)って呼ぶ。
(スレッド単位のシーケンスがどうなってるか絵にしたい)
main 別スレッド
|
|-------→|
|ここから先、メイン スレッドは自由
|
|
|→| 継続
|
・継続と戻り値
論理的にはほぼ一緒。
ただ、書き方がだいぶ煩わしく・・・
↓表にする
------------------------------
通常の戻り値
受け渡し側(関数内): return x;
受け取り側(呼ぶ側): var result = Method();
継続渡し
受け渡し側: continuation(x);
受け取り側: Method(x => ...);
------------------------------
戻り値を受け取る代わりに、継続処理をデリゲートで渡す
return で戻り値を返す代わりに、継続処理を呼び出す
再帰が深くなりそうに見えるけど、末尾呼び出しなので最適化可能。
・Combinator
戻り値⇔継続渡し の変換ができれば、非同期処理を同期っぽく書ける。
if とか while とかの制御構文も、If 関数とか While 関数作って、継続渡し化可能。
(こういうのを Combinator って言うらしい?関数型言語の用語。)
実際、F# の let! 非同期処理はこの 戻り値⇔継続渡し 変換で実現してるみたい。
http://tomasp.net/blog/async-compilation-internals.aspx
(この方法、正常フローの間はまだいいけども、例外処理絡むとどんどん面倒に)
・余談
.NET Framework は末尾呼び出しの最適化用の補助命令持ってる(Tail 命令)
メソッド呼び出し命令の前に Tail を付けると、スタック解放してから次のメソッド呼び出ししてくれる。
F# とかではこの Tail 命令入るし、
Tail 命令が吐いてなくても、.NET 4の64ビット版では自動的に末尾呼び出しの最適化してくれる。
(Release ビルド時のみ)
