await 中の Task をキャンセルしてみる
こんにちは。ゴールデンウィークになっても春休みの課題を終わらせていない azyobuzin です。最近また C# 関連記事が書けるようになってきました。……ええ、 Android の進捗ダメです。
さて、なぜか Owner 権限を持っている CoreTweet プロジェクトで async
に対応しようとしたところ ContinueWith
と TaskCompletionSource
にいじめられまくってるわけですが、ここまで頑張ってキャンセルを実装したところで、 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(); } } } }
これを実行すると、 catch
の Debugger.Break()
に到達してブレークします。
例外は OperationCanceledException
で、スタックトレースを見てみると、 18 行目、 throw
の行で例外が発生したことになっています。
また Task
は IsCanceled
, IsCompleted
が true
、 IsFaulted
が false
となっています。
まあ予想通りだと思います。 OperationCanceledException
で catch
してあげれば、キャンセルされたことを受け取れるように出来ます。
独自のキャンセル例外を用意してみる
次に
class MyTestCancelException : OperationCanceledException { }
という例外クラスを用意して、さっきのコードの throw
の行を書き換えてみます。
例外は MyTestCancelException
(スクリーンショットと違うのは許して)で、
Task
の状態はさっきと同じです。
……え? Task
の Exception
が null
なのに独自エラーがそのまま返ってくる!?!?!?これはキモい。
ContinueWith でキャンセルしてみる
今度は Task
の生成をこんな感じにしてみましょう。
Task.Run(() => Debug.WriteLine("I'm an async man.")) .ContinueWith(_ => { throw new OperationCanceledException(); })
すると…?
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);
このときの 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
でつなげるとちゃんと Canceled
な Task
が返ってくるし、そのまま complete ってなるし意味がわからない。