2015-09-11 30 views
2

美好的一天!我正在爲WinForms UI編寫一個幫助程序庫。使用TPL異步/的await機制啓動,得到了一個問題,這樣的代碼示例:如何處理TPL中的任務取消

private SynchronizationContext _context; 

    public void UpdateUI(Action action) 
    { 
     _context.Post(delegate { action(); }, null); 
    } 


    private async void button2_Click(object sender, EventArgs e) 
    { 

     var taskAwait = 4000; 
     var progressRefresh = 200; 
     var cancellationSource = new System.Threading.CancellationTokenSource(); 

     await Task.Run(() => { UpdateUI(() => { button2.Text = "Processing..."; }); }); 

     Action usefulWork =() => 
     { 
      try 
      { 
       Thread.Sleep(taskAwait); 
       cancellationSource.Cancel(); 
      } 
      catch { } 
     }; 
     Action progressUpdate =() => 
     { 
      int i = 0; 
      while (i < 10) 
      { 
       UpdateUI(() => { button2.Text = "Processing " + i.ToString(); }); 
       Thread.Sleep(progressRefresh); 
       i++; 
      } 
      cancellationSource.Cancel(); 
     }; 

     var usefulWorkTask = new Task(usefulWork, cancellationSource.Token); 
     var progressUpdateTask = new Task(progressUpdate, cancellationSource.Token); 

     try 
     { 
      cancellationSource.Token.ThrowIfCancellationRequested(); 
      Task tWork = Task.Factory.StartNew(usefulWork, cancellationSource.Token); 
      Task tProgress = Task.Factory.StartNew(progressUpdate, cancellationSource.Token); 
      await Task.Run(() => 
      { 
       try 
       { 
        var res = Task.WaitAny(new[] { tWork, tProgress }, cancellationSource.Token);       
       } 
       catch { } 
      }).ConfigureAwait(false); 
     } 
     catch (Exception ex) 
     { 
     } 
     await Task.Run(() => { UpdateUI(() => { button2.Text = "button2"; }); }); 
    } 

基本上,這個想法是運行兩個並行任務 - 一個是,比如說,進度條或任何更新和排序的超時控制器,另一個是長時間運行的任務本身。無論哪個任務先完成取消另一個任務。所以,取消「進度」任務應該沒有問題,因爲它有一個循環,在該循環中我可以檢查任務是否被標記爲取消。問題在於長時間運行的問題。它可以是Thread.Sleep()或SqlConnection.Open()。當我運行CancellationSource.Cancel()時,長時間運行的任務繼續工作,不會取消。在超時之後,我對長時間運行的任務或任何可能導致的結果不感興趣。
由於混亂的代碼示例可能會提示,我嘗試了一堆變體,但都沒有給出我想要的效果。像Task.WaitAny()這樣的東西凍結UI ...有沒有辦法讓這種取消工作,或者甚至可能是一種不同的方法來編碼這些東西?

UPD:

public static class Taskhelpers 
{ 
    public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken) 
    { 
     var tcs = new TaskCompletionSource<bool>(); 
     using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs)) 
     { 
      if (task != await Task.WhenAny(task, tcs.Task)) 
       throw new OperationCanceledException(cancellationToken); 
     } 
     return await task; 
    } 
    public static async Task WithCancellation(this Task task, CancellationToken cancellationToken) 
    { 
     var tcs = new TaskCompletionSource<bool>(); 
     using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs)) 
     { 
      if (task != await Task.WhenAny(task, tcs.Task)) 
       throw new OperationCanceledException(cancellationToken); 
     } 
     await task; 
    } 
} 

.....

 var taskAwait = 4000; 
     var progressRefresh = 200; 
     var cancellationSource = new System.Threading.CancellationTokenSource(); 
     var cancellationToken = cancellationSource.Token; 

     var usefulWorkTask = Task.Run(async() => 
     { 
      try 
      { 
       System.Diagnostics.Trace.WriteLine("WORK : started"); 

       await Task.Delay(taskAwait).WithCancellation(cancellationToken); 

       System.Diagnostics.Trace.WriteLine("WORK : finished"); 
      } 
      catch (OperationCanceledException) { } // just drop out if got cancelled 
      catch (Exception ex) 
      { 
       System.Diagnostics.Trace.WriteLine("WORK : unexpected error : " + ex.Message); 
      } 
     }, cancellationToken); 

     var progressUpdatetask = Task.Run(async() => 
     { 
      for (var i = 0; i < 25; i++) 
      { 
       if (!cancellationToken.IsCancellationRequested) 
       { 
        System.Diagnostics.Trace.WriteLine("==== : " + i.ToString()); 
        await Task.Delay(progressRefresh); 
       } 
      } 
     },cancellationToken); 

     await Task.WhenAny(usefulWorkTask, progressUpdatetask); 

     cancellationSource.Cancel(); 

通過修改for (var i = 0; i < 25; i++)限制i我模仿長期運行的任務是否進度任務或以其他方式之前完成。按需要工作。 WithCancellation輔助方法完成這項工作,儘管現在兩種'嵌套'Task.WhenAny看起來很可疑。

回答

4

我與保羅的回答所有的點一致 - 即利用先進的解決方案(Task.Run而不是Task.Factory.StartNewProgress<T>的最新進展,而不是手動發佈到SynchronizationContext,Task.WhenAny而不是Task.WaitAny用於異步代碼)。

但是,爲了回答實際問題:

當我運行CancellationSource.Cancel(),長時間運行的任務繼續工作並不會取消。超時後,我沒有興趣在長期運行的任務或任何可能導致

有兩個環節進行。

  • 如何編寫響應取消請求的代碼?
  • 如何編寫在取消後忽略任何響應的代碼?

注意與第一部分涉及取消操作,而第二部分實際處理取消等待的操作完成

首先要做的是:支持操作本身取消。對於CPU限制的代碼(即運行一個循環),定期調用token.ThrowIfCancellationRequested()。對於I/O綁定代碼,最好的選擇是將token傳遞給下一個API層 - 大多數(但不是全部)I/O API可以(應該)取消取消令牌。如果這不是一個選項,那麼您可以選擇忽略取消,或者您可以使用token.Register註冊取消回叫。有時您可以從您的Register回調中調用單獨的取消方法,並且有時可以通過從回調中丟棄對象來使其工作(這種方法通常是有效的,因爲長期以來Win32 API傳統取消了所有的I/O當該手柄關閉時處理)。不過,我不確定這是否適用於SqlConnection.Open

接下來,取消等待。這是一個比較簡單的,如果你只是想取消等待由於超時

await Task.WhenAny(tWork, tProgress, Task.Delay(5000)); 
+0

'等待Task.WhenAny(tWork,tProgress,Task.Delay(5000));'非常簡單直接! –

+0

@SergeMisnik:是的。如果你想要真正觀察一個真正的取消標記,它會變得相當複雜一點,但如果它只是一個超時,那麼它很簡單。 :) –

1

我認爲你需要在progressUpdate行動中檢查IsCancellationRequested

至於如何做到你想要的,this blog討論了擴展方法WithCancellation,這將使你停止等待你長時間運行的任務。

+3

在這個博客的一個重要概念是,你*不*取消不支持取消操作。你只能停止等待迴應。在某些情況下,您可以放棄任何需要很長時間的事情,例如通過中止線程或關閉數據庫連接。在其他情況下,您不能,例如,您可以中止REST或Web服務請求。您只能停止等待服務器響應。雖然服務器不會知道你已經停止等待 –

+0

'WithCancellation'方法實際上效果很好 –

2

當您在button2_Click上編寫類似await Task.Run(() => { UpdateUI(() => { button2.Text = "Processing..."; }); });的東西時,您將從UI線程調度操作到線程輪詢線程,該線程輪詢線程將操作發佈到UI線程。如果你直接調用動作,它會更快,因爲它不會有兩個上下文切換。

ConfigureAwait(false)導致同步上下文不被捕獲。我不應該在UI方法中使用它,因爲最可靠的是,你需要爲延續做一些UI工作。

除非您絕對有理由,否則不應使用Task.Factory.StartNew而不是Task.Run。見thisthis

對於進度更新,請考慮使用Progress<T> class,因爲它捕獲同步上下文。

也許你應該嘗試這樣的事:

private async void button2_Click(object sender, EventArgs e) 
{ 
    var taskAwait = 4000; 
    var cancellationSource = new CancellationTokenSource(); 
    var cancellationToken = cancellationSource.Token; 

    button2.Text = "Processing..."; 

    var usefullWorkTask = Task.Run(async() => 
     { 
      try 
      { 
       await Task.Dealy(taskAwait); 
      } 
      catch { } 
     }, 
     cancellationToken); 

    var progress = new Progress<imt>(i => { 
     button2.Text = "Processing " + i.ToString(); 
    }); 

    var progressUpdateTask = Task.Run(async() => 
     { 
      for(var i = 0; i < 10; i++) 
      { 
       progress.Report(i); 
      } 
     }, 
     cancellationToken); 

    await Task.WhenAny(usefullWorkTask, progressUpdateTask); 

    cancellationSource.Cancel(); 
} 
+0

我剛纔讀了S.Toub的文章,但最近無法找到它們重新閱讀。所以,必須做一個隨機的嘗試錯誤的方法...不是一個好的學習方式。我會試一試你的代碼。 –