例えば以下のようにawait / asyncで時間のかかる処理を非同期に動かしている途中で、
キャンセルボタン等によって動作を中断して終わらせるにはどうのようにしたら良いでしょうか
C#
1private async void button_Click(object sender, EventArgs e) 2{ 3 int intValue = await GetintValue.ToString(); 4} 5private async Task<int> GetintValue() 6{ 7 await //何か重い処理 8 return 1; 9}
重い処理は内部が隠蔽されているクラスライブラリを想定しています。
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
回答2件
0
ベストアンサー
こんにちは。
async/await、Taskで「正しく」非同期キャンセルをやるなら、CancellationTokenを使います。
GetintValueメソッドに引数としてCancellationTokenを受け取るようにして、メソッド内部でIsCancellationRequestedプロパティかThrowIfCancellationRequested()メソッドを用いて途中キャンセル時のフローを構築します。
await //何か重い処理
の重い処理
の内部で中断するには、重い処理
にも再帰的にCancellationTokenを引き渡していきます。一般的な非同期ライブラリのAPIであれば、時間を要する可能性がある処理には大抵CancellationTokenを受け入れるオーバーロードが存在しています。
csharp
1private async void button_Click(object sender, EventArgs e) 2{ 3 int intValue = await GetintValue(どこかのCancellationToken).ToString(); 4} 5 6private async Task<int> GetintValue(CancellationToken token) 7{ 8 await 何か重い処理(token); // 重い処理にもtokenを渡す 9 10 if (token.IsCancellationRequested) 11 return -1; // キャンセル状態か判定して何かしたり 12 13 token.ThrowIfCancellationRequested(); // キャンセル状態だったらいきなり例外をぶっ放して止めたり 14 15 return 1; 16}
それぞれの非同期メソッドに渡すCancellationTokenは基本的にCancellationTokenSourceから生成します。そして、CancellationTokenSourceのCancel()メソッドを呼ぶことでtokenがCanceled状態になります。
csharp
1// 仮にフィールドに置く 2private readonly CancellationTokenSource cts; 3 4// ... 5{ 6 await GetintValue(this.cts.Token); // ctsからtokenを取り出して渡す 7} 8 9// ...キャンセルボタンとか 10{ 11 this.cts.Canel(); // どこかでCancel()メソッドを呼ぶと非同期に走っているGetintValueメソッドがキャンセルされる 12} 13
あと、ThrowIfCancellationRequested()メソッドを用いてキャンセルする場合、非同期メソッドのTaskの状態がRunningでもCompletedでもなくCanceled状態になるため、キャンセルされたということが明確になるためオススメです。
追記
TaskではなくIAsyncActionの非同期キャンセルということだったので追記します、おそらくTaskより簡単に処理できます。(UWP関係はほぼ触ったことないので間違ってるかもしれません)
IAsyncActionインターフェースはそれ自体がCancel()メソッドを持っており、かつAwaitableになっているものです。
awaitを使って非同期メソッドのフローに組み込む前に、それ自身のキャンセル機構を呼び出せるように設計するだけで良いです。
csharp
1private IAsyncInfo _asyncTask; // とりあえず非同期処理そのものを監視するための変数 2 3{ 4 var scan = _adapter.ScanAsync(); // まずawaitをつけないで実行し、IAsyncActionを取得 5 this._asyncTask = scan; // 先にフィールドとか外部に渡す 6 await scan; // その後await 7} 8 9// ...Cancelボタン 10{ 11 this._asyncTask?.Cancel(); // IAsyncInfoのCancel()メソッドを呼ぶだけ 12}
さらに別の方法をとして、通常のTaskのように、CancellationTokenを使ったキャンセル機構に一元化したい場合に、CancellationToken.Register()メソッドを利用することもできます。
以下はもっと良い方法を教えていただいたので末尾に追記してます。
csharp
1async Task GetintValue(CancellationToken token) 2{ 3 var scan = _adapter.ScanAsync(); // まずawaitをつけないで実行 4 token.Register(() => scan.Cancel()); // tokenがCancel状態になったときにScanAsyncをキャンセルするように登録 5 await scan; // その後await 6} 7 8// ...Cancelボタン 9{ 10 this.cts.Cancel(); // 普通のTaskのようにCancellationTokenSourceのCancel()メソッドを呼んでキャンセルできる 11}
さらに追記
IAsyncActionをTaskとして取り扱える拡張メソッドが存在することを教えていただきました。(asmさんありがとうございます)
これを利用するとCancellationTokenを利用するキャンセル処理を自然に実装できます。async/awaitやTaskと組み合わせる場合では、これが一番良い方法だと思います。
csharp
1async Task GetintValue(CancellationToken token) // 引数で受け取って 2{ 3 await _adapter.ScanAsync().AsTask(token); // AsTaskメソッドにtokenを渡して、awaitするだけ 4} 5 6// ...Cancelボタン 7{ 8 this.cts.Cancel(); // 普通のTaskのようにCancellationTokenSourceのCancel()メソッドを呼んでキャンセルできる 9}
投稿2018/03/07 16:55
編集2018/03/08 06:46総合スコア4228
0
こんにちは。
私は、terminatedフラグを用意します。初期値はfalseで中断させたい時にtrueを設定します。そして、「何か重い処理」の中で時々それをチェックし、trueだったら中断させます。
なお、terminatedフラグをtrueにした後、安易にGatintValue()の終了を待つと確実にデッドロックするので要注意です。
ケースバイケースで対処してます。なかなか一般的な解が見つかっていませんが、私はSynchronizationContextを使うことが多いです。
投稿2018/03/07 09:00
総合スコア23272
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2018/03/08 01:32
2018/03/08 02:56
2018/03/08 02:59
2018/03/08 05:43
2018/03/08 08:32 編集
2018/03/08 09:34