2017-08-29 60 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)。試想一下,如果「任務」在某個對象上進行鎖定,並且在它被釋放之前被取消,或者該任務正在處理非託管內存 –