如何取消等待中的任务?


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 '18

Answers:


239

阅读上取消(这是在.NET 4.0中引入的,是基本不变从那时起)和基于任务的异步模式,它提供了关于如何使用的指导方针CancellationTokenasync方法。

总而言之,您将a传递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;
}

2
哇好消息!效果很好,现在我需要弄清楚如何在async方法中处理异常。谢啦!我将阅读您建议的内容。
卡洛

8
不能。大多数长时间运行的同步方法都有一些取消它们的方法-有时通过关闭基础资源或调用另一个方法来取消它们。CancellationToken具有与自定义取消系统互操作所需的所有挂钩,但是没有什么可以取消无法取消的方法。
Stephen Cleary 2012年

1
啊,我明白了。因此,捕获ProcessCancelledException的最佳方法是通过在try / catch中包装“ await”?有时我会收到AggregatedException,但无法处理。
卡洛

3
对。我建议您不要使用WaitResultasync方法中使用;您应该始终使用await它来正确解开异常。
Stephen Cleary 2012年

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
看起来非常棘手...作为整个async-await实现。我认为这样的构造不会使源代码更具可读性。
Maxim

1
谢谢您,请注意一件事-应稍后处理注册令牌,第二件事-请使用ConfigureAwait否则会在UI应用程序中受伤。
astrowalker

@astrowalker:是的,令牌的注册确实最好不进行注册(处置)。这可以在传递给Register()的委托中完成,方法是在Register()返回的对象上调用dispose。但是,由于在这种情况下“源”令牌仅是本地令牌,因此无论如何都将清除所有内容……
sonatique

1
实际上,只需将其嵌套在中即可using
天行者

@astrowalker ;-)是的,您实际上是对的。在这种情况下,这是简单得多的解决方案!但是,如果您希望直接(不等待)返回Task.WhenAny,则需要其他东西。我之所以这样说,是因为我曾经遇到过这样的重构问题:在我使用...等待之前。然后我删除了await(以及函数上的异步),因为它是唯一的一个,却没有注意到我已经完全破坏了代码。由此产生的错误很难找到。因此,我不愿意将using()与async / await一起使用。我觉得Dispose模式与异步事物都无法很好地
相处

15

尚未涉及的一种情况是如何在异步方法内部处理取消。以一个简单的案例为例,您需要将一些数据上载到服务中,以获取它来计算某些东西,然后返回一些结果。

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在可能的情况下,最好将选项一直传递下去。
Miral

1
@Miral是正确的,但是有许多异步方法都没有取消标记。以WCF服务为例,当您使用异步方法生成客户端时,该服务将不包括取消令牌。确实如示例所示,并且正如Stephen Cleary也指出的那样,假定长时间运行的同步任务可以通过某种方式取消它们。
kjbartel

1
这就是为什么我说“尽可能”。通常,我只是希望提及此警告,以便以后找到此答案的人们不会有错误的印象。
Miral

@Miral谢谢。我已更新以反映此警告。
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;
}

使用此路由,所有处理已为您完成,取消任务时,它仅触发事件处理程序,您可以查看是否已在该处取消它。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.