目次

キーワード

概要

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 ビルド時のみ)

    

更新履歴

ブログ