【问题标题】:How to cancel a Task in await?如何取消等待中的任务?
【发布时间】:2022-02-23 03:47:48
【问题描述】:

我正在使用这些 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()
{
}

【问题讨论】:

  • 刚刚找到this article,它帮助我了解了各种取消方式。

标签: c# task-parallel-library .net-4.5


【解决方案1】:

阅读 Cancellation(在 .NET 4.0 中引入,此后基本未更改)和 Task-Based Asynchronous Pattern,它提供了有关如何使用 CancellationTokenasync 方法的指南。

总而言之,您将CancellationToken 传递给每个支持取消的方法,并且该方法必须定期检查它。

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;
}

【讨论】:

  • 哇很棒的信息!效果很好,现在我需要弄清楚如何处理异步方法中的异常。谢啦!我会阅读你建议的内容。
  • 嘿伙计,如果我无法使用慢速方法,有没有办法做到这一点?例如,假设 slowFunc 在黑盒中,您只能调用该方法,而不能修改其中的任何内容?
  • 没有。大多数长时间运行的同步方法都有 some 方法来取消它们 - 有时通过关闭底层资源或调用另一个方法。 CancellationToken 具有与自定义取消系统互操作所需的所有挂钩,但没有什么可以取消不可取消的方法。
  • 对。我recommend,你从不在async 方法中使用WaitResult;您应该始终使用 await 代替,这样可以正确解开异常。
  • 只是好奇,为什么没有一个例子使用CancellationToken.IsCancellationRequested,而是建议抛出异常?
【解决方案2】:

或者,为了避免修改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());

【讨论】:

  • 它看起来很棘手......作为整个 async-await 实现。我不认为这样的结构会使源代码更具可读性。
  • 谢谢,一个注意事项——注册令牌应该稍后处理,第二件事——使用ConfigureAwait,否则你可能会在 UI 应用程序中受到伤害。
  • @astrowalker :是的,确实应该最好取消注册(处置)令牌的注册。这可以通过对 Register() 返回的对象调用 dispose 在传递给 Register() 的委托中完成。但是,由于“源”令牌在这种情况下只是本地的,所以无论如何都会清除所有内容......
  • 实际上只需将其嵌套在using中即可。
  • @astrowalker ;-) 是的,实际上你是对的。在这种情况下,这是更简单的解决方案!但是,如果您希望直接返回 Task.WhenAny(不等待),那么您需要其他东西。我这样说是因为我曾经遇到过这样的重构问题:在我使用...等待之前。然后我删除了 await (以及函数上的 async ),因为它是唯一的,没有注意到我完全破坏了代码。由此产生的错误很难找到。因此,我不愿意将 using() 与 async/await 一起使用。我觉得 Dispose 模式无论如何都不适用于异步事物......
【解决方案3】:

尚未涵盖的一个案例是如何在异步方法中处理取消。举个简单的例子,你需要将一些数据上传到一个服务,让它计算一些东西,然后返回一些结果。

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,以便它最终可以处理取消。试图中止线程isn't recommended

【讨论】:

  • 请注意,虽然这会取消等待任务,但它不会取消实际任务(例如,UploadDataAsync 可能会在后台继续,但一旦完成就不会进行调用到CalculateAsync,因为那部分已经停止等待)。这对您来说可能有问题,也可能没有问题,特别是如果您想重试该操作。如果可能,将CancellationToken 一直向下传递是首选选项。
  • @Miral 确实如此,但是有许多异步方法不采用取消令牌。以 WCF 服务为例,当您使用异步方法生成客户端时,这些服务将不包含取消令牌。事实上,正如示例所示,正如 Stephen Cleary 也指出的那样,假设长时间运行的同步任务有一些方法可以取消它们。
  • 这就是我说“尽可能”的原因。大多数情况下,我只是想提一下这个警告,以便以后找到这个答案的人不会产生错误的印象。
  • @Miral 谢谢。我已更新以反映此警告。
  • 遗憾的是,这不适用于“NetworkStream.WriteAsync”等方法。
【解决方案4】:

我只想添加到已经接受的答案。我被困在这一点上,但我在处理完整事件时采取了不同的方式。我没有运行 await,而是向任务添加了一个已完成的处理程序。

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;
}

使用这条路由,所有的处理都已经为你完成了,当任务被取消时,它只会触发事件处理程序,你可以在那里查看它是否被取消。

【讨论】:

    猜你喜欢
    • 2017-10-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-04-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-10-15
    相关资源
    最近更新 更多