我们的应用程序使用 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”任务对我来说感觉非常笨拙。它还有一个缺点,就是迫使我重构以这种方式排队工作的代码的所有部分,因为必须将所有长时间运行的调用包装在一个新任务中。
尝试 #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();
}
}
这再次有效,但现在我有两个问题:
a) 感觉就像我在滥用 TaskCompletionSource,从不使用它的结果,并且在我完成工作时设置为 null。
b) 为了正确连接延续,我需要处理前一个工作单元的唯一 TaskCompletionSource,而不是为其创建的任务。这在技术上是可行的,但又让人觉得笨重和奇怪。
然后去哪儿?
重申一下,我的问题是:这些方法中的任何一种都是解决这个问题的“正确”方法,还是有更正确/优雅的解决方案可以让我过早中止长期运行的任务并立即开始继续?我更喜欢低影响的解决方案,但如果这是正确的做法,我愿意进行一些大规模的重构。
或者,TPL 是否是该工作的正确工具,或者我是否缺少更好的任务排队机制。我的目标框架是 .NET 4.0。