2011-09-08 60 views
44

從我讀過的關於任務的內容中,下面的代碼應該取消當前正在執行的任務而不會引發異常。我的印象是,取消任務的全部任務是禮貌地「要求」任務停止而不中止線程。取消任務引發異常

從下面的程序的輸出是:

傾銷例外

[OperationCanceledException]

取消和上次計算素返回。

我試圖在取消時避免任何異常。我怎樣才能做到這一點?

void Main() 
{ 
    var cancellationToken = new CancellationTokenSource(); 

    var task = new Task<int>(() => { 
     return CalculatePrime(cancellationToken.Token, 10000); 
    }, cancellationToken.Token); 

    try 
    { 
     task.Start(); 
     Thread.Sleep(100); 
     cancellationToken.Cancel(); 
     task.Wait(cancellationToken.Token);   
    } 
    catch (Exception e) 
    { 
     Console.WriteLine("Dumping exception"); 
     e.Dump(); 
    } 
} 

int CalculatePrime(CancellationToken cancelToken, object digits) 
{ 
    int factor; 
    int lastPrime = 0; 

    int c = (int)digits; 

    for (int num = 2; num < c; num++) 
    { 
     bool isprime = true; 
     factor = 0; 

     if (cancelToken.IsCancellationRequested) 
     { 
      Console.WriteLine ("Cancelling and returning last calculated prime."); 
      //cancelToken.ThrowIfCancellationRequested(); 
      return lastPrime; 
     } 

     // see if num is evenly divisible 
     for (int i = 2; i <= num/2; i++) 
     { 
      if ((num % i) == 0) 
      {    
       // num is evenly divisible -- not prime 
       isprime = false; 
       factor = i; 
      } 
     } 

     if (isprime) 
     { 
      lastPrime = num; 
     } 
    } 

    return lastPrime; 
} 
+0

如果您嘗試避免異常,則不應使用ThrowIfCancellationRequested引發異常。只需從CalculatePrime中優雅地返回即可。 –

+0

我已經刪除了cancelToken.ThrowIfCancellationRequested();我仍然得到相同的結果。 –

+0

不要將取消令牌傳遞給Task.wait,因爲您已請求取消該任務。改爲調用Task.wait()。 –

回答

50

你被明確在這條線拋出一個異常:

cancelToken.ThrowIfCancellationRequested(); 

如果要正常退出的任務,那麼你只需要擺脫線。

通常人們使用它作爲控制機制來確保當前處理被中止而不會潛在地運行任何額外的代碼。此外,也沒有必要調用,因爲它ThrowIfCancellationRequested()當檢查取消在功能上等同於:

if (token.IsCancellationRequested) 
    throw new OperationCanceledException(token); 

當使用ThrowIfCancellationRequested()你的任務可能看起來更像是這樣的:

int CalculatePrime(CancellationToken cancelToken, object digits) { 
    try{ 
     while(true){ 
      cancelToken.ThrowIfCancellationRequested(); 

      //Long operation here... 
     } 
    } 
    finally{ 
     //Do some cleanup 
    } 
} 

此外,Task.Wait(CancellationToken)將拋出如果令牌被取消,則爲異常。要使用此方法,您需要將等待呼叫包裝在Try...Catch區塊中。

MSDN: How to Cancel a Task

+2

謝謝喬希。在循環的每次迭代中調用ThrowIfCancellationRequested()而不是加倍並檢查取消標誌是有意義的。 –

+17

作爲警告:用一堆IsCancellationRequested檢查代替'ThrowIfCancellationRequested'正常退出,正如答案所述。但這不僅僅是一個實現細節;影響可觀察行爲:任務將不再以取消狀態結束,而是在'RanToCompletion'中。並且* *不僅可以影響顯式狀態檢查,而且還可以更微妙地影響例如任務的鏈接。 'ContinueWith',具體取決於所使用的TaskContinuationOptions。我會說避免「ThrowIfCancellationRequested」是一個危險的建議。 –

+0

「如果令牌被取消,Task.Wait(CancellationToken)將引發異常。」這是我的問題。謝謝。 – granadaCoder

69

我想取消時避免任何異常。

你不應該那樣做。

投擲OperationCanceledException是TPL中表達的「您所稱的方法被取消」的慣用方式。不要反對 - 只是期待它。

這是一個的事情,因爲這意味着當你有多個操作使用相同的取消標記,你不需要在每個級別胡椒你的代碼與檢查,看看你的方法剛纔所說的實際上已經正常完成,還是因爲取消而返回。你可以在任何地方都使用使用CancellationToken.IsCancellationRequested,但是從長遠來看,它會讓你的代碼不那麼優雅。

需要注意的是有你的榜樣的代碼片段被拋出一個異常 - 一個任務本身:

cancelToken.ThrowIfCancellationRequested() 

和一個在那裏你等待任務完成:

task.Wait(cancellationToken.Token); 

我不認爲你真的想將取消標記傳遞給task.Wait調用,說實話...允許其他代碼取消您在等待。鑑於你知道你剛剛取消了這個標記,這是毫無意義的 - 這是綁定引發異常,無論該任務是否實際上已注意到取消還是沒有。選項:

  • 使用不同取消標記(這樣其他的代碼可以取消獨立的等待)
  • 使用超時
  • 只是等待,只要它需要
+0

Jon輸入try的成本高於if(IsCancellationRequested)。在正常的項目中,你會忽略那些對你所說的好處。然而,如果你想避免任何成本額外的成本,那麼你實際上不能使用取消,並需要手動建立它........我相信他們可以實現一些ThrowIfCancelled屬性爲那些誰想要例外。但不是。 –

+8

@Bobb:注意屬性('IsCancellationRequested')和方法('ThrowIfCancellationRequested')*均在TPL中實現。所以在非常極端的情況下,你真的不想要異常的代價,你可以避免它 - 但是如果沒有*證據*,它實際上就是系統中的一個瓶頸。 –

+2

我目前遇到了這個問題,主要是因爲取消看起來像一個正常的操作,並使用正常流量控制異常* stll *感覺不對。我錯過了什麼? –

6

關於使用ThrowIfCancellationRequested而不是IsCancellationRequested的好處的另一個說明:我發現當需要使用ContinueWith以及延續選項TaskContinuationOptions.OnlyOnCanceled時,IsCancellationRequested不會導致條件ContinueWith發射。 ThrowIfCancellationRequested,但是,設置任務的取消條件,導致ContinueWith發射。

注意:只有當任務已經運行,而不是任務開始時纔會發生這種情況。這就是爲什麼我在開始和取消之間添加Thread.Sleep()

CancellationTokenSource cts = new CancellationTokenSource(); 

Task task1 = new Task(() => { 
    while(true){ 
     if(cts.Token.IsCancellationRequested) 
      break; 
    } 
}, cts.Token); 
task1.ContinueWith((ant) => { 
    // Perform task1 post-cancellation logic. 
    // This will NOT fire when calling cst.Cancel(). 
} 

Task task2 = new Task(() => { 
    while(true){ 
     cts.Token.ThrowIfCancellationRequested(); 
    } 
}, cts.Token); 
task2.ContinueWith((ant) => { 
    // Perform task2 post-cancellation logic. 
    // This will fire when calling cst.Cancel(). 
} 

task1.Start(); 
task2.Start(); 
Thread.Sleep(3000); 
cts.Cancel(); 
+0

這是因爲任務狀態只有在給定Exception類型退出時才被設置爲Cancelled,否則,如果您只是「返回」它的狀態,則其狀態將設置爲RanToCompletion。因此,OnlyOnCanceled代碼塊不會被調用。 – almulo

7

上述答案中的一部分看起來好像ThrowIfCancellationRequested()是一個選項。在這種情況下,它是而不是,因爲你不會得到你最後的素數。 idiomatic way that "the method you called was cancelled"被定義爲取消意味着丟棄任何(中間)結果的情況。如果您的取消定義是「停止計算並返回最後的中間結果」,那麼您已經離開了這個方向。

討論特別是在運行時方面的好處也是相當具有誤導性的: 實現的算法在運行時吸收。即使是高度優化的取消也無濟於事。

最簡單的優化將是展開這個循環,並跳過一些不必要的循環:

for(i=2; i <= num/2; i++) { 
    if((num % i) == 0) { 
    // num is evenly divisible -- not prime 
    isprime = false; 
    factor = i; 
    } 
} 

您可以

  • 保存(NUM/2)-1個週期爲每個偶數,這是整體略小於50%(展開),
  • 保存(num/2)-square_root_of(num)個循環,對於每個素數(根據最小素數因數選擇界限),
  • 至少爲每個非素數節省很多,預計會節省更多,例如,num = 999以1週期而不是499結束(中斷,如果找到答案)和
  • 保存另外50%的循環,這當然是整體的25%(根據質數的數學選擇步驟,展開處理特例2)。

即佔節約75%的最低保證(粗略估計:90%)的循環在內部循環,只是通過用替換它:

if ((num % 2) == 0) { 
    isprime = false; 
    factor = 2; 
} else { 
    for(i=3; i <= (int)Math.sqrt(num); i+=2) { 
    if((num % i) == 0) { 
     // num is evenly divisible -- not prime 
     isprime = false; 
     factor = i; 
     break; 
    } 
    } 
} 

有快得多的算法(其我不會討論,因爲我遠離主題),但這種優化很容易,但仍然證明了我的觀點: 當您的算法這個遠不是最優時,不要擔心微優化運行時間。

+0

+1,但是我不確定你的回答是否正確,以及使用中的算法與正確的算法相比有多慢。他應該從最大數量開始,並使用[Baillie-PSW素數測試](http://en.wikipedia.org/wiki/Baillie%E2%80%93PSW_primality_test)進行處理。 –

+0

我認爲手頭的任務不是**解決了「在規定的計算時間內儘可能大地計算素數」的問題,而是通過例子學習編碼,例如「計算所有質數,直到取回的請求返回最大素數計算「。我不想開始討論隨機初步測試,因爲需求可能要求100%確保檢測到的素數。對於大質數,這將導致確定性的AKS素性測試,如果我沒有記錯的話,這個測試反過來使用或被Eratosthenes的篩子擊敗所有小於2^23的數字。 –

+2

對不起,我離題遠離我原本不想要的。然而,在我對「280Z28 Jan 15 at 1:37」的回答中缺少的一點是,保證節省內部循環的75%會使任何「**取消**運行時討論」過時。考慮到取消是每個定義每次調用只執行一次,所以不應該有許多應用程序需要擔心它的運行時間。 –