通过使用ConcurrentExclusiveSchedulerPair 实例的ExclusiveScheduler 属性,并在每次启动任务时将其用作TaskScheduler,在ThreadPool 上连续执行任务非常简单:
var taskFactory = new TaskFactory(
new ConcurrentExclusiveSchedulerPair().ExclusiveScheduler);
Task task1 = taskFactory.StartNew(() => DoSomething());
Task task2 = taskFactory.StartNew(() => DoSomethingElse());
DoSomething() 和DoSomethingElse 都将在ThreadPool 上运行,一个接一个。保证两个调用不会重叠,并且它们将按照最初安排的顺序被调用。
但是如果这些调用中的任何一个失败会发生什么?这就是问题所在:DoSomething() 或 DoSomethingElse 引发的任何异常都将被困在各自的 Task(task1 或 task2)内。这意味着我们不能只是开始任务而忘记它们。我们有责任将任务存储在某个地方,并最终await 他们并处理他们的异常。这可能正是我们想要的。
但是,如果我们只想安排任务并“忘记”它们,并且在不太可能的情况下,它们中的任何一个都无法将异常作为未处理的异常传播并终止进程,该怎么办?这并不像听起来那么疯狂。有些任务可能对应用程序的生命周期至关重要,而且它们不太可能失败,而且很难设计一种手动观察异常的策略,以至于它们的异常升级为即时应用程序终止(在引发AppDomain.UnhandledException 事件)可能是可用选项中较小的邪恶。那么有可能做到这一点吗?是的,但令人惊讶的困难和棘手:
using System.Runtime.ExceptionServices;
var taskFactory = new TaskFactory(
new ConcurrentExclusiveSchedulerPair().ExclusiveScheduler);
void RunOnThreadPoolExclusive(Action action)
{
_ = taskFactory.StartNew(() =>
{
try
{
action();
}
catch (Exception ex)
{
var edi = ExceptionDispatchInfo.Capture(ex);
ThreadPool.QueueUserWorkItem(_ => edi.Throw());
}
});
}
RunOnThreadPoolExclusive(() => DoSomething());
RunOnThreadPoolExclusive(() => DoSomethingElse());
action 在 try/catch 块中调用。如果失败,异常会在ExceptionDispatchInfo 实例中捕获,以保留其堆栈跟踪,然后在ThreadPool 上重新抛出。请注意,taskFactory.StartNew 仍然返回 Task,使用 discard (_) 将其丢弃,因为现在任务失败的可能性很小。但我们真的取得了任何进展吗?我们从DoSomething 不太可能失败的前提开始,我们最终放弃了我们评估为极不可能失败的Task。确实不是很满意!我们能做得更好吗?是的!进入async void的infamous世界:
var taskFactory = new TaskFactory(
new ConcurrentExclusiveSchedulerPair().ExclusiveScheduler);
async void RunOnThreadPoolExclusive(Action action)
{
await taskFactory.StartNew(action);
}
RunOnThreadPoolExclusive(() => DoSomething());
RunOnThreadPoolExclusive(() => DoSomethingElse());
async void 方法有一个有趣的特性,即在它们内部抛出的任何异常都会在 SynchronizationContext 上引发,该异常是在 async void 方法启动时捕获的,或者(作为后备)在 ThreadPool 上引发。因此,例如,如果在 WinForms 应用程序的 UI 线程上调用 RunOnThreadPoolExclusive,并且操作失败,则会弹出一个消息框,询问用户是否要继续或退出应用程序 (screenshot)。所以错误不一定是致命的,因为用户可能会选择忽略错误并继续。这可能正是我们想要的。也可能不会。
为了澄清,错误将在 UI 线程上抛出,但 DoSomething()/DoSomethingElse() 仍将在 ThreadPool 上调用。这没有改变。
那么,我们究竟如何确保错误将在ThreadPool 上抛出,而其他任何地方,无论如何,不管当前上下文如何,并且不允许任何 任务触发-然后忘记?方法如下:
var taskFactory = new TaskFactory(
new ConcurrentExclusiveSchedulerPair().ExclusiveScheduler);
void RunOnThreadPoolExclusive(Action action)
{
Task task = taskFactory.StartNew(action);
ThreadPool.QueueUserWorkItem(async state => await (Task)state, task);
}
RunOnThreadPoolExclusive(() => DoSomething());
RunOnThreadPoolExclusive(() => DoSomethingElse());
在ThreadPool 上以正确的顺序序列化执行,在ThreadPool 上抛出错误,并且没有泄露的即发即弃任务。完美!