在TPL中中止长时间运行的任务

我们的应用程序使用TPL来序列化(可能)长时间运行的工作单元。 工作(任务)的创建是用户驱动的,可以随时取消。 为了拥有响应式用户界面,如果不再需要当前的工作,我们想放弃我们正在做的事情,并立即开始一项不同的任务。

任务排队等同于:

private Task workQueue; private void DoWorkAsync (Action 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的状态那样简单,如果/当检测到取消时, 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”任务对我来说感觉非常笨拙。 它还有一个缺点,就是迫使我重构我的代码的所有部分,以这种方式排队工作,因为需要在新的Task中包装所有长时间运行的调用。

尝试#2:TaskCompletionSource修补

 static void Main(string[] args) { var tcs = new TaskCompletionSource(); //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(); } } 

这又有效,但现在我有两个问题:

a)感觉就像我滥用TaskCompletionSource从不使用它的结果,只是在我完成工作时设置为null。

b)为了正确连接延续,我需要处理上一个工作单元的唯一TaskCompletionSource,而不是为它创建的任务。 这在技术上是可行的,但再次感到笨拙和奇怪。

然后去哪儿?

重申一下,我的问题是:这些方法中的任何一种都是解决这个问题的“正确”方法,还是有更正确/更优雅的解决方案可以让我过早地中止长期运行的任务并立即开始延续? 我倾向于使用低影响力的解决方案,但如果这是正确的做法,我愿意进行一些巨大的重构。

或者,TPL甚至是工作的正确工具,还是我错过了更好的任务排队机制。 我的目标框架是.NET 4.0。

这里真正的问题是DoWork中长时间运行的呼叫不能识别取消。 如果我理解正确,那么你在这里做的并不是取消长时间运行的工作,而只是允许继续执行,当工作完成取消的任务时,忽略结果。 例如,如果您使用内部任务模式调用CrunchNumbers(),这需要几分钟,取消外部任务将允许继续发生,但CrunchNumbers()将继续在后台执行,直到完成。

除了让长时间运行的呼叫支持取消之外,我认为除此之外没有任何真正的方法。 通常这是不可能的(它们可能阻止API调用,没有API支持取消。)在这种情况下,它确实是API中的一个缺陷; 您可以检查是否存在可以用于以可以取消的方式执行操作的备用API调用。 一种黑客方法是在任务启动时捕获对Task使用的底层Thread的引用,然后调用Thread.Interrupt。 这将从各种睡眠状态唤醒线程并允许它终止,但是以可能令人讨厌的方式。 最糟糕的情况是,你甚至可以调用Thread.Abort,但这更有问题而且不推荐。


这是对基于委托的包装器的攻击。 这是未经测试的,但我认为它会起作用; 如果您使其工作并有修复/改进,请随时编辑答案。

 public sealed class AbandonableTask { private readonly CancellationToken _token; private readonly Action _beginWork; private readonly Action _blockingWork; private readonly Action _afterComplete; private AbandonableTask(CancellationToken token, Action beginWork, Action blockingWork, Action 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 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; } }