待機中のタスクをキャンセルする方法は?


164

私はこれらのWindows 8 WinRTタスクで遊んでいて、以下の方法を使用してタスクをキャンセルしようとしていますが、それはある時点で機能します。CancelNotificationメソッドが呼び出されると、タスクがキャンセルされたように見えますが、バックグラウンドでタスクは実行を続け、その後、タスクが完了すると、タスクのステータスは常に完了し、キャンセルされることはありません。キャンセルされたときにタスクを完全に停止する方法はありますか?

private async void TryTask()
{
    CancellationTokenSource source = new CancellationTokenSource();
    source.Token.Register(CancelNotification);
    source.CancelAfter(TimeSpan.FromSeconds(1));
    var task = Task<int>.Factory.StartNew(() => slowFunc(1, 2), source.Token);

    await task;            

    if (task.IsCompleted)
    {
        MessageDialog md = new MessageDialog(task.Result.ToString());
        await md.ShowAsync();
    }
    else
    {
        MessageDialog md = new MessageDialog("Uncompleted");
        await md.ShowAsync();
    }
}

private int slowFunc(int a, int b)
{
    string someString = string.Empty;
    for (int i = 0; i < 200000; i++)
    {
        someString += "a";
    }

    return a + b;
}

private void CancelNotification()
{
}

キャンセルするさまざまな方法を理解するのに役立つこの記事を見つけました。
Uwe Keim

回答:


239

上の記事を読むまでのキャンセル(.NET 4.0で導入されて以来、大幅に変更はありません)とタスクベースの非同期パターンを使用する方法のガイドラインを提供、CancellationTokenとのasync方法を。

要約すると、CancellationTokenキャンセルをサポートする各メソッドにaを渡し、そのメソッドは定期的にそれをチェックする必要があります。

private async Task TryTask()
{
  CancellationTokenSource source = new CancellationTokenSource();
  source.CancelAfter(TimeSpan.FromSeconds(1));
  Task<int> task = Task.Run(() => slowFunc(1, 2, source.Token), source.Token);

  // (A canceled task will raise an exception when awaited).
  await task;
}

private int slowFunc(int a, int b, CancellationToken cancellationToken)
{
  string someString = string.Empty;
  for (int i = 0; i < 200000; i++)
  {
    someString += "a";
    if (i % 1000 == 0)
      cancellationToken.ThrowIfCancellationRequested();
  }

  return a + b;
}

2
すごい情報!これで問題なく動作しました。次に、非同期メソッドで例外を処理する方法を理解する必要があります。ありがとう!あなたが提案したものを読みます。
Carlo

8
いいえ。ほとんどの長期実行同期メソッドにはそれらをキャンセルする方法がいくつかあります。基盤となるリソースを閉じたり、別のメソッドを呼び出したりすることがあります。CancellationTokenカスタムキャンセルシステムとの相互運用に必要なすべてのフックがありますが、キャンセルできないメソッドをキャンセルすることはできません。
スティーブンクリアリー2012

1
ああ、なるほど。したがって、ProcessCancelledExceptionをキャッチする最良の方法は、「待機」をtry / catchでラップすることですか?AggregatedExceptionが発生し、それを処理できない場合があります。
カルロ

3
正しい。私はお勧め、あなたが使用しないことWaitまたはResultasync方法。常にawait代わりに使用して、例外を正しくラップ解除する必要があります。
Stephen Cleary

11
好奇心旺盛ですが、どの例も使用CancellationToken.IsCancellationRequestedせず、例外をスローすることを提案する理由はありますか?
James M

41

または、変更を回避するためにslowFunc(たとえば、ソースコードにアクセスできない場合など):

var source = new CancellationTokenSource(); //original code
source.Token.Register(CancelNotification); //original code
source.CancelAfter(TimeSpan.FromSeconds(1)); //original code
var completionSource = new TaskCompletionSource<object>(); //New code
source.Token.Register(() => completionSource.TrySetCanceled()); //New code
var task = Task<int>.Factory.StartNew(() => slowFunc(1, 2), source.Token); //original code

//original code: await task;  
await Task.WhenAny(task, completionSource.Task); //New code

https://github.com/StephenCleary/AsyncExの素敵な拡張メソッドを使用して、次のようにシンプルにすることもできます

await Task.WhenAny(task, source.Token.AsTask());

1
それは非常にトリッキーに見えます...全体の非同期待機実装として。私はそのような構造がソースコードを読みやすくすることはないと思います。
Maximの

1
ありがとう、1つのメモ-登録トークンは後で破棄する必要があります、2番目に-使用しConfigureAwaitないと、UIアプリで損害を与える可能性があります。
astrowalker 2018年

@astrowalker:はい、確かにトークンの登録は登録解除(破棄)する必要があります。これは、Register()によって返されたオブジェクトに対してdisposeを呼び出すことにより、Register()に渡されるデリゲート内で実行できます。ただし、この場合「ソース」トークンはローカルのみであるため、とにかくすべてがクリアされます...
sonatique

1
実際に必要なのは、それを入れ子にすることだけですusing
astrowalker 2018年

@astrowalker ;-)はい、あなたは正しいです。この場合、これははるかに簡単な解決策です!ただし、Task.WhenAnyを直接(待機せずに)返したい場合は、別のものが必要です。私がこのようなリファクタリングの問題に一度遭遇したので、私はこれを言います。コードが完全に壊れていることに気付かずに、それが唯一のウェイトだったので、それからawait(および関数の非同期)を削除しました。結果のバグを見つけるのは困難でした。したがって、using()をasync / awaitと一緒に使用することに消極的です。とにかく、Disposeパターンは非同期のものにはうまくいかないと思います...
sonatique '15年

15

カバーされていない1つのケースは、非同期メソッド内でキャンセルを処理する方法です。たとえば、サービスにデータをアップロードして何かを計算し、結果を返す必要がある単純なケースを考えてみましょう。

public async Task<Results> ProcessDataAsync(MyData data)
{
    var client = await GetClientAsync();
    await client.UploadDataAsync(data);
    await client.CalculateAsync();
    return await client.GetResultsAsync();
}

キャンセルをサポートする場合、最も簡単な方法は、トークンを渡して、各非同期メソッド呼び出し間で(またはContinueWithを使用して)キャンセルされたかどうかを確認することです。それらが非常に長時間実行される呼び出しである場合でも、キャンセルされるまでしばらく待機している可能性があります。キャンセルされるとすぐに失敗する小さなヘルパーメソッドを作成しました。

public static class TaskExtensions
{
    public static async Task<T> WaitOrCancel<T>(this Task<T> task, CancellationToken token)
    {
        token.ThrowIfCancellationRequested();
        await Task.WhenAny(task, token.WhenCanceled());
        token.ThrowIfCancellationRequested();

        return await task;
    }

    public static Task WhenCanceled(this CancellationToken cancellationToken)
    {
        var tcs = new TaskCompletionSource<bool>();
        cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).SetResult(true), tcs);
        return tcs.Task;
    }
}

したがって、それを使用するには.WaitOrCancel(token)、任意の非同期呼び出しに追加します。

public async Task<Results> ProcessDataAsync(MyData data, CancellationToken token)
{
    Client client;
    try
    {
        client = await GetClientAsync().WaitOrCancel(token);
        await client.UploadDataAsync(data).WaitOrCancel(token);
        await client.CalculateAsync().WaitOrCancel(token);
        return await client.GetResultsAsync().WaitOrCancel(token);
    }
    catch (OperationCanceledException)
    {
        if (client != null)
            await client.CancelAsync();
        throw;
    }
}

これは、待機していたタスクを停止せず、実行を継続することに注意してください。あなたのような、それを停止する別のメカニズムを使用する必要がありますCancelAsync例の呼び出し、またはより良いまだ同じに合格CancellationTokenするTaskことが最終的にキャンセルを処理できるようにします。スレッドを中止することはお勧めしません


1
これはタスクの待機をキャンセルしますが、実際のタスクはキャンセルしません(たとえばUploadDataAsync、バックグラウンドで続行することはできますが、一度完了するとCalculateAsync、その部分はすでに待機を停止しているため、呼び出しを行いません)。これは、特に操作を再試行する場合に問題となる可能性があります。CancellationToken可能な限り、ずっと下に渡すことをお勧めします。
ミラル

1
@Miralはtrueですが、キャンセルトークンを取得しない非同期メソッドが多数あります。たとえば、非同期メソッドを使用してクライアントを生成するときにキャンセルトークンが含まれないWCFサービスを見てみましょう。実際、例が示すように、またStephen Clearyも述べたように、長時間実行される同期タスクには、それらをキャンセルする何らかの方法があると想定されています。
kjbartel

1
だからこそ「可能なとき」と言った。ほとんどの場合、後でこの答えを見つけた人が間違った印象を受けないように、この警告に言及したかっただけです。
ミラル

@ミラルありがとう。この警告を反映するように更新しました。
kjbartel

残念ながら、これは 'NetworkStream.WriteAsync'などのメソッドでは機能しません。
Zeokat

6

私はすでに受け入れられている答えに追加したいだけです。私はこれで立ち往生しましたが、完全なイベントを処理するために別のルートを進んでいました。待機するのではなく、完了したハンドラーをタスクに追加します。

Comments.AsAsyncAction().Completed += new AsyncActionCompletedHandler(CommentLoadComplete);

イベントハンドラーは次のようになります。

private void CommentLoadComplete(IAsyncAction sender, AsyncStatus status )
{
    if (status == AsyncStatus.Canceled)
    {
        return;
    }
    CommentsItemsControl.ItemsSource = Comments.Result;
    CommentScrollViewer.ScrollToVerticalOffset(0);
    CommentScrollViewer.Visibility = Visibility.Visible;
    CommentProgressRing.Visibility = Visibility.Collapsed;
}

このルートでは、すべての処理が既に行われています。タスクがキャンセルされると、イベントハンドラーがトリガーされ、そこでキャンセルされたかどうかを確認できます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.