背景:
我有一个 Web 应用程序,可以启动长时间运行(且无状态)的任务:
var task = Task.Run(() => await DoWork(foo))
task.Wait();
因为它们运行时间较长,所以我需要能够通过单独的网络请求取消它们。
为此,我想使用 CancellationToken,并在令牌被取消后立即抛出异常。然而,据我所知,任务取消是合作,这意味着任务正在运行的代码必须显式检查令牌以查看是否已发出取消请求(例如
CancellationToken.ThrowIfCancellation()
)
我想避免到处检查
CancellationToken.ThrowIfCancellation()
,因为任务很长并且涉及很多功能。我认为我可以完成我想要创建显式 Thread
的任务,但我真的很想避免手动线程管理。这就是说...
问题: 任务被取消时是否可以自动抛出异常,如果没有,是否有任何好的替代方案(模式等)来减少对代码的污染
CancellationToken.ThrowIfCancellation()
?
我想避免这样的事情:
async Task<Bar> DoWork(Foo foo)
{
CancellationToken.ThrowIfCancellation()
await DoStuff1();
CancellationToken.ThrowIfCancellation()
await DoStuff2();
CancellationToken.ThrowIfCancellation()
await DoStuff3();
...
}
我觉得这个问题与这个问题有很大不同,因为我明确要求一种方法来最小化检查取消令牌的调用,接受的答案对此做出响应“时不时地,在函数内部,调用令牌.ThrowIfCancellationRequested()"
任务被取消时是否可以自动抛出异常,如果不能,是否有任何好的替代方案(模式等)来减少 CancellationToken.ThrowIfCancellation() 对代码的污染?
不,不。所有取消都是合作的。取消代码的最佳方法是让代码响应取消请求。这是唯一好的模式。
我想我可以完成我想要创建的显式线程
其实不然。
此时,问题是“如何取消不可取消的代码?”答案取决于您希望系统的稳定性:
Thread
和 Abort
线程中运行代码。这是最容易实现的,但在应用程序不稳定方面也是最危险的。坦率地说,如果您在应用程序中的任何位置调用Abort
,除了心跳/烟雾测试检查等标准做法之外,您还应该定期重新启动该应用程序。AppDomain
和 Unload
AppDomain 中运行代码。这更难实现(您必须使用远程处理),并且在核心世界中不是一个选项。事实证明,AppDomain
甚至无法像预期那样保护包含的应用程序,因此任何使用此技术的应用程序也需要定期重新启动。Process
和 Kill
进程中运行代码。这是实现起来最复杂的,因为您还需要实现某种形式的进程间通信。但这是取消不可取消代码的唯一可靠的解决方案。如果你放弃不稳定的解决方案(1)和(2),那么唯一剩下的解决方案(3)是一个吨的工作 - 方式,远远超过使代码可取消。
TL;DR:只需按照设计的方式使用取消 API 即可。这是最简单有效的解决方案。
如果您实际上只有一堆方法调用,您一个接一个地调用,您可以实现一个方法运行程序,按顺序运行它们并检查它们之间的取消情况。
类似这样的:
public static void WorkUntilFinishedOrCancelled(CancellationToken token, params Action[] work)
{
foreach (var workItem in work)
{
token.ThrowIfCancellationRequested();
workItem();
}
}
你可以这样使用它:
async Task<Bar> DoWork(Foo foo)
{
WorkUntilFinishedOrCancelled([YourCancellationToken], DoStuff1, DoStuff2, DoStuff3, ...);
}
这基本上可以实现你想要的。
如果您对Thread.Abort
的
影响(一次性未处置、锁未释放、应用程序状态损坏)感到满意,那么您可以通过以下方式通过中止任务的专用线程来实现非合作取消。
private static Task<TResult> RunAbortable<TResult>(Func<TResult> function,
CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<TResult>();
var thread = new Thread(() =>
{
try
{
TResult result;
using (cancellationToken.Register(Thread.CurrentThread.Abort))
{
result = function();
}
tcs.SetResult(result);
}
catch (ThreadAbortException)
{
tcs.TrySetCanceled();
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
});
thread.IsBackground = true;
thread.Start();
return tcs.Task;
}
使用示例:
var cts = new CancellationTokenSource();
var task = RunAbortable(() => DoWork(foo), cts.Token);
task.Wait();
ControlledExecution.Run
,与上面所示的 RunAbortable
具有类似的功能。它调用 Action
而不是 Func<TResult>
,并且该操作是在当前线程而不是专用线程上调用的。当取消 CancellationToken
时,执行将中止,但当前线程不会被终止,因为中止是在 finally
块内部进行 重置(源代码)。