アジョブジ星通信

日常系バンザイ。

await 中の Task をキャンセルしてみる

f:id:azyobuzin:20140501200545p:plain

こんにちは。ゴールデンウィークになっても春休みの課題を終わらせていない azyobuzin です。最近また C# 関連記事が書けるようになってきました。……ええ、 Android の進捗ダメです。

さて、なぜか Owner 権限を持っている CoreTweet プロジェクトで async に対応しようとしたところ ContinueWithTaskCompletionSource にいじめられまくってるわけですが、ここまで頑張ってキャンセルを実装したところで、 await での動作はどうなるんだろう?と思っていくらか試してみました。

普通にキャンセルしてみる

ご存知の通り Task.Run 内で OperationCanceledException を投げればキャンセル扱いになるので、まずは普通にやってみましょう。

using System;
using System.Diagnostics;
using System.Threading.Tasks;

namespace TaskCancelTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Do().Wait();
        }

        static async Task Do()
        {
            var t = Task.Run(() =>
            {
                throw new OperationCanceledException();
            });
            try
            {
                await t;
                Debugger.Break();
            }
            catch
            {
                Debugger.Break();
            }
        }
    }
}

これを実行すると、 catchDebugger.Break() に到達してブレークします。

f:id:azyobuzin:20140501201755p:plain

例外は OperationCanceledException で、スタックトレースを見てみると、 18 行目、 throw の行で例外が発生したことになっています。

f:id:azyobuzin:20140501202039p:plain

また TaskIsCanceled, IsCompletedtrueIsFaultedfalse となっています。

まあ予想通りだと思います。 OperationCanceledExceptioncatch してあげれば、キャンセルされたことを受け取れるように出来ます。

独自のキャンセル例外を用意してみる

次に

class MyTestCancelException : OperationCanceledException { }

という例外クラスを用意して、さっきのコードの throw の行を書き換えてみます。

f:id:azyobuzin:20140501202647p:plain

例外は MyTestCancelExceptionスクリーンショットと違うのは許して)で、

f:id:azyobuzin:20140501202814p:plain

Task の状態はさっきと同じです。

……え? TaskExceptionnull なのに独自エラーがそのまま返ってくる!?!?!?これはキモい。

ContinueWith でキャンセルしてみる

今度は Task の生成をこんな感じにしてみましょう。

Task.Run(() => Debug.WriteLine("I'm an async man."))
    .ContinueWith(_ =>
    {
        throw new OperationCanceledException();
    })

すると…?

f:id:azyobuzin:20140501203403p:plain

Faulted だ!!

ContinueWith では CancellationToken のみでキャンセルが可能なようです。

(追記)OperationCanceledException のコンストラクタに CancellationToken を渡すことでキャンセル判定にしてくれます。

しかし、キャンセルの処理は相当うまくて、最初からキャンセル要求された CancellationToken が渡された場合、前の Task も実行しないようにしてくれるようです。この検証コードの source.Cancel() の位置をいろいろいじると楽しいかも。

var source = new CancellationTokenSource();
var t = Task.Run(() =>
{
    Debug.WriteLine("I'm an async man.");
    source.Cancel();
}).ContinueWith(_ =>
{
    Debug.WriteLine("ContinueWith");
}, source.Token);

f:id:azyobuzin:20140501204743p:plain

このときの Task の状態は Canceled で、例外は TaskCanceledExceptionスタックトレースawait の内部で呼び出される TaskAwaiter.GetResult となっています。

例外を吐かずにキャンセルしてみる

みんな大好き(吐血) TaskCompletionSource を使って遅延キャンセルをやってみます。

static Task<string> MakeTask()
{
    var tcs = new TaskCompletionSource<string>();
    Task.Delay(500).ContinueWith(_ => tcs.TrySetCanceled());
    return tcs.Task;
}

結果は…… TaskCanceledException です。はい、 ContinueWith をやったときになんとなく察しはついてました。

まとめ

いまいち Task 系は非 public なところでごにょごにょしているようで見通しが悪いですね。とにかくキャンセルを捕まえるにはどんな実装になっていようと、 OperationCanceledException を受け取れば良さそうです。

うーん、 CoreTweet の非同期コードを LINQPad で試すとキャンセルしても awaiting から返ってこないのはどこか実装が間違ってるのかな。。 ContinueWith でつなげるとちゃんと CanceledTask が返ってくるし、そのまま complete ってなるし意味がわからない。