2017-08-29 81 views
1

我遇到了一个问题,如何正确取消异步任务。是否有正确的方法来取消异步任务?

这是一些草案。

我的入口点运行两个异步任务。第一项任务做了一些“长期”的工作,第二项任务取消了它。

切入点:

private static void Main() 
{ 
    var ctc = new CancellationTokenSource(); 

    var cancellable = ExecuteLongCancellableMethod(ctc.Token); 

    var cancelationTask = Task.Run(() => 
    { 
     Thread.Sleep(2000); 

     Console.WriteLine("[Before cancellation]"); 

     ctc.Cancel(); 
    }); 

    try 
    { 
     Task.WaitAll(cancellable, cancelationTask); 
    } 
    catch (Exception e) 
    { 
     Console.WriteLine($"An exception occurred with type {e.GetType().Name}"); 
    } 
} 

方法,返回可取消的任务:

private static Task ExecuteLongCancellableMethod(CancellationToken token) 
{ 
    return Task.Run(() => 
    { 
     token.ThrowIfCancellationRequested(); 

     Console.WriteLine("1st"); 
     Thread.Sleep(1000); 

     Console.WriteLine("2nd"); 
     Thread.Sleep(1000); 

     Console.WriteLine("3rd"); 
     Thread.Sleep(1000); 

     Console.WriteLine("4th"); 
     Thread.Sleep(1000); 

     Console.WriteLine("[Completed]"); 

    }, token); 
} 

我的目的是停止写入 '1', '第二', '3'立即在取消之后被调用。 但我得到以下结果:

1st 
2nd 
3rd 
[Before cancellation] 
4th 
[Completed] 

出于显而易见的原因,我没有得到请求取消时抛出异常。于是,我就重写方法如下:

private static Task ExecuteLongCancellableAdvancedMethod(CancellationToken token) 
{ 
    return Task.Run(() => 
    { 
     var actions = new List<Action> 
     { 
      () => Console.WriteLine("1st"), 
      () => Console.WriteLine("2nd"), 
      () => Console.WriteLine("3rd"), 
      () => Console.WriteLine("4th"), 
      () => Console.WriteLine("[Completed]") 
     }; 

     foreach (var action in actions) 
     { 
      token.ThrowIfCancellationRequested(); 

      action.Invoke(); 

      Thread.Sleep(1000); 
     } 

    }, token); 
} 

现在我得到了我想要的:

1st 
2nd 
[Before cancellation] 
3rd 
An exception occurred with type AggregateException 

,但我想在创建动作代表的集合和循环通过它是不是最方便处理我的问题的方法。

那么,正确的方法是什么?为什么我需要将取消令牌作为第二个参数传递给Task.Run方法?

+0

如果令牌取消了已经申请,'Task.Run'不会跑委托在所有 –

+1

您需要显式调用'token.ThrowIfCancellationRequested();'在您想要的每一个地方执行实际上是可以取消的。所以在你的第一个例子中,在每个'Console.WriteLine'之前。 –

+1

我强烈建议你阅读ContinueWith方法,因为你问到“正确”的方式,我想你会想要熟悉ContinueWith和TaskContinuationOptions.NotOnCanceled。以下是包含更多有用信息的链接:https://social.msdn.microsoft.com/Forums/zh-CN/310782d8-aadf-4841-a309-23abff407d9a/cancellation-using-exception-to-control-application-flow?论坛= parallelextensions – Jace

回答

2

Task不会取消它的自我,这是你检测到取消请求,并干净地放弃你的工作。这就是token.ThrowIfCancellationRequested();所做的。

您应该将这些检查放在整个代码中,执行可以干净地停止或回滚到安全状态的地方。

在你的第二个例子中,你在循环的每次迭代中调用它一次,并且它工作正常。第一个例子只在一开始就调用一次。如果该标记尚未被该点取消,则该任务将运行至完成状态,就像您看到的一样。

如果您将其更改为这样,您也会看到您期望的结果。

return Task.Run(() => 
{ 
    token.ThrowIfCancellationRequested(); 
    Console.WriteLine("1st"); 
    Thread.Sleep(1000); 

    token.ThrowIfCancellationRequested(); 
    Console.WriteLine("2nd"); 
    Thread.Sleep(1000); 

    token.ThrowIfCancellationRequested(); 
    Console.WriteLine("3rd"); 
    Thread.Sleep(1000); 

    token.ThrowIfCancellationRequested(); 
    Console.WriteLine("4th"); 
    Thread.Sleep(1000); 

    Console.WriteLine("[Completed]"); 

}, token); 
+0

谢谢,我知道我写的代码是如何工作的,但有没有更方便的方法来取消任务?我的意思是在每个逻辑块之后写'ThrowIfCancellationRequested'使得代码看起来很糟糕,至于我 –

+1

不,我不知道。只有开发人员可以知道代码中的安全位置是干净取消“任务”的位置。 “Task”中的代码可能会使变量和数据处于不确定的未知状态,如果它在取消请求时以某种方式取消,并且[这可能与Thread.Abort一样危险(https ://stackoverflow.com/questions/1559255/whats-wrong-with-using-thread-abort)。试想一下,如果“任务”在某个对象上进行锁定,并且在它被释放之前被取消,或者该任务正在处理非托管内存 –