2011-01-19 71 views
17

我們的應用程序使用TPL序列化(可能)長時間運行的工作單元。工作(任務)的創建是由用戶驅動的,可能隨時取消。爲了有一個響應式用戶界面,如果不再需要當前的工作,我們想放棄我們正在做的事情,並立即開始一項不同的工作。在TPL中取消長時間運行的任務

任務排隊等候是這樣的:

private Task workQueue; 
private void DoWorkAsync 
    (Action<WorkCompletedEventArgs> callback, CancellationToken token) 
{ 
    if (workQueue == null) 
    { 
     workQueue = Task.Factory.StartWork 
      (() => DoWork(callback, token), token); 
    } 
    else 
    { 
     workQueue.ContinueWork(t => DoWork(callback, token), token); 
    } 
} 

DoWork方法中包含一個長期運行的通話,所以它不是那麼簡單,經常檢查token.IsCancellationRequested狀態與脫困如果/當檢測到取消。長時間運行的工作將阻止任務繼續,直至完成,即使任務被取消。

我想出了兩個解決此問題的示例方法,但我不相信這兩個方法都是正確的。我創建了簡單的控制檯應用程序來演示它們如何工

需要注意的重要一點是繼續在原始任務完成之前觸發

嘗試#1:一個內部任務

static void Main(string[] args) 
{ 
    CancellationTokenSource cts = new CancellationTokenSource(); 
    var token = cts.Token; 
    token.Register(() => Console.WriteLine("Token cancelled")); 
    // Initial work 
    var t = Task.Factory.StartNew(() => 
    { 
     Console.WriteLine("Doing work"); 

     // Wrap the long running work in a task, and then wait for it to complete 
     // or the token to be cancelled. 
     var innerT = Task.Factory.StartNew(() => Thread.Sleep(3000), token); 
     innerT.Wait(token); 
     token.ThrowIfCancellationRequested(); 
     Console.WriteLine("Completed."); 
    } 
    , token); 
    // Second chunk of work which, in the real world, would be identical to the 
    // first chunk of work. 
    t.ContinueWith((lastTask) => 
     { 
      Console.WriteLine("Continuation started"); 
     }); 

    // Give the user 3s to cancel the first batch of work 
    Console.ReadKey(); 
    if (t.Status == TaskStatus.Running) 
    { 
     Console.WriteLine("Cancel requested"); 
     cts.Cancel(); 
     Console.ReadKey(); 
    } 
} 

這工作,但 「innerT」 任務感到非常kludgey給我。它還有一個缺點,就是迫使我重新構造以這種方式排隊工作的代碼的所有部分,因爲必須在新任務中包含所有長時間運行的調用。

嘗試#2:TaskCompletionSource修修補補

static void Main(string[] args) 
{ var tcs = new TaskCompletionSource<object>(); 
//Wire up the token's cancellation to trigger the TaskCompletionSource's cancellation 
    CancellationTokenSource cts = new CancellationTokenSource(); 
    var token = cts.Token; 
    token.Register(() => 
     { Console.WriteLine("Token cancelled"); 
      tcs.SetCanceled(); 
      }); 
    var innerT = Task.Factory.StartNew(() => 
     { 
      Console.WriteLine("Doing work"); 
      Thread.Sleep(3000); 
      Console.WriteLine("Completed."); 
    // When the work has complete, set the TaskCompletionSource so that the 
    // continuation will fire. 
      tcs.SetResult(null); 
     }); 
    // Second chunk of work which, in the real world, would be identical to the 
    // first chunk of work. 
    // Note that we continue when the TaskCompletionSource's task finishes, 
    // not the above innerT task. 
    tcs.Task.ContinueWith((lastTask) => 
     { 
     Console.WriteLine("Continuation started"); 
     }); 
    // Give the user 3s to cancel the first batch of work 
    Console.ReadKey(); 
    if (innerT.Status == TaskStatus.Running) 
    { 
     Console.WriteLine("Cancel requested"); 
     cts.Cancel(); 
     Console.ReadKey(); 
    } 
} 

同樣,這工作,但現在我有兩個問題:

一)感覺就像我濫用TaskCompletionSource通過從來沒有使用它的結果,當我完成我的工作時就設置爲空。

b)爲了正確連接繼續,我需要保留前一個工作單元的獨特TaskCompletionSource的句柄,而不是爲其創建的任務。這在技術上是可行的,但又感到笨重和奇怪。

下一步該怎麼辦?

重申,我的問題是:這些方法中的哪一種都是解決這個問題的「正確」方法,還是有更正確/優雅的解決方案,可以讓我過早地中止長時間運行的任務並立即開始延續?我的偏好是低影響的解決方案,但如果這是正確的做法,我願意承擔一些巨大的重構。

或者,TPL甚至是工作的正確工具,還是我錯過了更好的任務排隊機制。我的目標框架是.NET 4.0。

+0

我也問這個問題在這裏:http://social.msdn.microsoft.com/Forums/en/parallelextensions/thread/d0bcb415-fb1e-42e4-90f8-c43a088537fb – 2011-01-21 13:58:03

回答

8

這裏真正的問題是,DoWork中的長時間呼叫不支持取消。如果我理解正確,那麼你在這裏做的並不是真的取消長時間運行的工作,而只是允許繼續執行,並且當工作完成時取消的任務忽略結果。例如,如果使用內部任務模式調用CrunchNumbers()(需要幾分鐘時間),則取消外部任務將允許繼續進行,但CrunchNumbers()將繼續在後臺執行直至完成。

我不認爲有任何真正的解決這個問題的辦法,而不是讓長時間運行的呼叫支持取消。通常這是不可能的(它們可能阻塞了API調用,沒有API支持取消)。當這種情況發生時,它確實是API中的一個缺陷;您可以檢查是否有替代API調用可用於以可取消的方式執行操作。對此的一種破解方法是在任務啓動時捕獲對任務使用的底層線程的引用,然後調用Thread.Interrupt。這將喚醒線程從各種睡眠狀態,並允許它終止,但以一種潛在的討厭的方式。最糟糕的情況是,你甚至可以調用Thread.Abort,但這更有問題,不推薦。


這是對基於委託的包裝器的刺傷。這是未經測試的,但我認爲它會做到這一點。隨時編輯答案,如果你使它的工作和修復/改進。

public sealed class AbandonableTask 
{ 
    private readonly CancellationToken _token; 
    private readonly Action _beginWork; 
    private readonly Action _blockingWork; 
    private readonly Action<Task> _afterComplete; 

    private AbandonableTask(CancellationToken token, 
          Action beginWork, 
          Action blockingWork, 
          Action<Task> afterComplete) 
    { 
     if (blockingWork == null) throw new ArgumentNullException("blockingWork"); 

     _token = token; 
     _beginWork = beginWork; 
     _blockingWork = blockingWork; 
     _afterComplete = afterComplete; 
    } 

    private void RunTask() 
    { 
     if (_beginWork != null) 
      _beginWork(); 

     var innerTask = new Task(_blockingWork, 
           _token, 
           TaskCreationOptions.LongRunning); 
     innerTask.Start(); 

     innerTask.Wait(_token); 
     if (innerTask.IsCompleted && _afterComplete != null) 
     { 
      _afterComplete(innerTask); 
     } 
    } 

    public static Task Start(CancellationToken token, 
          Action blockingWork, 
          Action beginWork = null, 
          Action<Task> afterComplete = null) 
    { 
     if (blockingWork == null) throw new ArgumentNullException("blockingWork"); 

     var worker = new AbandonableTask(token, beginWork, blockingWork, afterComplete); 
     var outerTask = new Task(worker.RunTask, token); 
     outerTask.Start(); 
     return outerTask; 
    } 
} 
+0

你的理解是正確的。我們完全可以讓我們的「CrunchNumbers」運行完成,但是如果出現這種結果,它將被忽略。 – 2011-01-20 16:11:19