マネージ スレッドのキャンセルに関するドキュメントを読むことを常にお勧めします。完全ではありません。ほとんどの MSDN ドキュメントと同様に、何をすべきかではなく、何ができるかを示します。しかし、キャンセルに関するドットネットのドキュメントよりも明確です。
例は基本的な使用法を示しています
まず、コード例のキャンセルはタスクをキャンセルするだけであり、基になる操作をキャンセルしないことに注意することが重要です。これを行わないことを強くお勧めします。
操作をキャンセルしたい場合は、取得するように更新する必要がありますRunFoo
(CancellationToken
使用方法については以下を参照してください)。
public Task Run(IFoo foo, CancellationToken token = default(CancellationToken)) {
var tcs = new TaskCompletionSource<object>();
// Regular finish handler
EventHandler<AsyncCompletedEventArgs> callback = (sender, args) =>
{
if (args.Cancelled)
{
tcs.TrySetCanceled(token);
CleanupFoo(foo);
}
else
tcs.TrySetResult(null);
};
RunFoo(foo, token, callback);
return tcs.Task;
}
キャンセルできない場合foo
は、API がキャンセルをまったくサポートしていません。
public Task Run(IFoo foo) {
var tcs = new TaskCompletionSource<object>();
// Regular finish handler
EventHandler<EventArgs> callback = (sender, args) => tcs.TrySetResult(null);
RunFoo(foo, callback);
return tcs.Task;
}
これは、このシナリオに適したコード手法です (キャンセルされるのは待機であり、タスクによって表される操作ではないため) 。「キャンセル可能な待機」の実行は、私の AsyncEx.Tasks ライブラリを介して実行できます。または、独自の同等の拡張メソッドを作成することもできます。
ドキュメントには、単に戻ってタスク ステータスを RanToCompletion に切り替えるか、OperationCanceledException (タスクの結果が Canceled になる) をスローするだけで問題ないと書かれています。
ええ、それらのドキュメントは誤解を招くものです。まず、ただ戻らないでください。実際には操作が正常に完了しなかった場合でも、メソッドはタスクを正常に完了し、操作が正常に完了したことを示します。これは一部のコードでは機能する可能性がありますが、一般的には良い考えではありません。
通常、a に応答する適切な方法CancellationToken
は次のいずれかです。
- を定期的に呼び出します
ThrowIfCancellationRequested
。このオプションは、CPU バウンドのコードに適しています。
- 経由でキャンセル コールバックを登録します
Register
。このオプションは、I/O バウンドのコードに適しています。登録は破棄する必要があることに注意してください。
あなたの特定のケースでは、異常な状況があります。あなたの場合、私は3番目のアプローチを取ります:
- 「フレームごとの作業」で、チェックし
token.IsCancellationRequested
ます。要求された場合は、AsyncCompletedEventArgs.Cancelled
set toでコールバック イベントを発生させtrue
ます。
これは、最初の適切な方法 (定期的に を呼び出す) と論理的に同等でThrowIfCancellationRequested
あり、例外をキャッチし、それをイベント通知に変換します。例外なく。
タスクが返されるのを待っていると、常に TaskCanceledException が発生します。私の推測では、これは通常の動作であり (そうであることを願っています)、キャンセルを使用する場合は、呼び出しの周りに try/catch をラップすることが期待されます。
キャンセル可能なタスクの適切な消費await
コードは、 try/catchおよび catchOperationCanceledException
でラップすることです。さまざまな理由 (多くの歴史的理由) により、一部の API が原因OperationCanceledException
となり、一部の API が原因となりTaskCanceledException
ます。からTaskCanceledException
派生しているためOperationCanceledException
、消費するコードは、より一般的な例外をキャッチできます。
でも、ユーザーが正常に終了したタスクとキャンセルされたタスクを区別したい場合、[キャンセル例外] はそれを保証するための副作用ですよね?
はい、それは受け入れられたパターンです。
ドキュメントによると、キャンセルに関連する例外であっても、TPL によって AggregateException にラップされることが示唆されています。
これは、コードがタスクで同期的にブロックする場合にのみ当てはまります。そもそもこれを避けるべきです。したがって、ドキュメントは間違いなく誤解を招くものです。
ただし、私のテストでは、ラッパーなしで、常に TaskCanceledException を直接取得していました。
await
AggregateException
ラッパーを回避します。
コメント説明のための更新CleanupFoo
はキャンセル方法です。
CancellationToken
最初に、によって開始されたコード内で直接使用することをお勧めしRunFoo
ます。そのアプローチはほぼ確実に簡単になります。
ただし、やむを得ずキャンセルする場合はCleanupFoo
、キャンセルが必要ですRegister
。その登録を破棄する必要があります。これを行う最も簡単な方法は、実際には 2 つの異なる方法に分割することです。
private Task DoRun(IFoo foo) {
var tcs = new TaskCompletionSource<object>();
// Regular finish handler
EventHandler<EventArgs> callback = (sender, args) => tcs.TrySetResult(null);
RunFoo(foo, callback);
return tcs.Task;
}
public async Task Run(IFoo foo, CancellationToken token = default(CancellationToken)) {
var tcs = new TaskCompletionSource<object>();
using (token.Register(() =>
{
tcs.TrySetCanceled(token);
CleanupFoo();
});
{
var task = DoRun(foo);
try
{
await task;
tcs.TrySetResult(null);
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
}
await tcs.Task;
}
リソースのリークを防ぎながら、結果を適切に調整して伝播することは、非常に厄介です。コードをCancellationToken
直接使用できる場合は、はるかにクリーンになります。