2011-01-19 101 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