如何允许Taskexception传播回UI线程?
在TPL中,如果任务抛出exception,则捕获该exception并将其存储在Task.Exception中 ,然后遵循有关观察到的exception的所有规则。 如果它从未被观察过,它最终会在终结器线程上重新抛出并使进程崩溃。
有没有办法阻止任务捕获该exception,只是让它传播?
我感兴趣的任务已经在UI线程上运行(由TaskScheduler.FromCurrentSynchronizationContext提供 ),我希望exception能够转义,因此它可以由我现有的Application.ThreadException处理程序处理。
我基本上希望Task中未处理的exception在按钮单击处理程序中表现得像未处理的exception:立即在UI线程上传播,并由ThreadException处理。
好的乔…正如所承诺的,这里是你如何使用自定义TaskScheduler
子类来解决这个问题。 我已经测试了这个实现,它就像一个魅力。 如果你想看到Application.ThreadException
实际开火!!! 不要忘记你不能附加调试器!
Custom TaskScheduler
这个自定义TaskScheduler实现在“诞生”时绑定到特定的SynchronizationContext
,并将获取它需要执行的每个传入Task
,将一个Continuation链接到它,只有在逻辑Task
故障时才会触发,当它触发时,它会Post
。返回SynchronizationContext,它将从出现故障的Task
中抛出exception。
public sealed class SynchronizationContextFaultPropagatingTaskScheduler : TaskScheduler { #region Fields private SynchronizationContext synchronizationContext; private ConcurrentQueue taskQueue = new ConcurrentQueue (); #endregion #region Constructors public SynchronizationContextFaultPropagatingTaskScheduler() : this(SynchronizationContext.Current) { } public SynchronizationContextFaultPropagatingTaskScheduler(SynchronizationContext synchronizationContext) { this.synchronizationContext = synchronizationContext; } #endregion #region Base class overrides protected override void QueueTask(Task task) { // Add a continuation to the task that will only execute if faulted and then post the exception back to the synchronization context task.ContinueWith(antecedent => { this.synchronizationContext.Post(sendState => { throw (Exception)sendState; }, antecedent.Exception); }, TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously); // Enqueue this task this.taskQueue.Enqueue(task); // Make sure we're processing all queued tasks this.EnsureTasksAreBeingExecuted(); } protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) { // Excercise for the reader return false; } protected override IEnumerable GetScheduledTasks() { return this.taskQueue.ToArray(); } #endregion #region Helper methods private void EnsureTasksAreBeingExecuted() { // Check if there's actually any tasks left at this point as it may have already been picked up by a previously executing thread pool thread (avoids queueing something up to the thread pool that will do nothing) if(this.taskQueue.Count > 0) { ThreadPool.UnsafeQueueUserWorkItem(_ => { Task nextTask; // This thread pool thread will be used to drain the queue for as long as there are tasks in it while(this.taskQueue.TryDequeue(out nextTask)) { base.TryExecuteTask(nextTask); } }, null); } } #endregion }
关于此实施的一些说明/免责声明:
- 如果你使用无参数构造函数,它将获取当前的SynchronizationContext …所以如果你只是在WinForms线程(主窗体构造函数,无论如何)上构造它,它将自动工作。 Bonus,我还有一个构造函数,你可以在其中显式传入你从其他地方获得的SynchronizationContext。
- 我还没有提供TryExecuteTaskInline的实现,所以这个实现只是总是将要处理的
Task
排队。 我把这作为读者的练习。 这并不难,只是……没有必要展示你所要求的function。 - 我正在使用一种简单/原始的方法来调度/执行利用ThreadPool的Tasks。 肯定有更丰富的实现,但这个实现的重点只是关于将exception封送回“应用程序”线程
好的,现在您有几个使用此TaskScheduler的选项:
预配置TaskFactory实例
此方法允许您设置一次TaskFactory
,然后您使用该工厂实例启动的任何任务将使用自定义TaskScheduler
。 这基本上看起来像这样:
在应用程序启动
private static readonly TaskFactory MyTaskFactory = new TaskFactory(new SynchronizationContextFaultPropagatingTaskScheduler());
整个代码
MyTaskFactory.StartNew(_ => { // ... task impl here ... });
显式的TaskScheduler Per-Call
另一种方法是创建自定义TaskScheduler
的实例,然后在每次启动任务时将其传递到默认TaskFactory
上的StartNew
。
在应用程序启动
private static readonly SynchronizationContextFaultPropagatingTaskScheduler MyFaultPropagatingTaskScheduler = new SynchronizationContextFaultPropagatingTaskScheduler();
整个代码
Task.Factory.StartNew(_ => { // ... task impl here ... }, CancellationToken.None // your specific cancellationtoken here (if any) TaskCreationOptions.None, // your proper options here MyFaultPropagatingTaskScheduler);
我发现一种解决方案在某些时候可以充分发挥作用。
单一任务
var synchronizationContext = SynchronizationContext.Current; var task = Task.Factory.StartNew(...); task.ContinueWith(task => synchronizationContext.Post(state => { if (!task.IsCanceled) task.Wait(); }, null));
这会在UI线程上调度对task.Wait()
的调用。 因为我不知道任务已经完成,所以它不会做Wait
,它实际上不会阻塞; 它只会检查是否有exception,如果有,它会抛出。 由于SynchronizationContext.Post
回调直接从消息循环(在Task
的上下文之外)执行,因此TPL不会停止exception,并且它可以正常传播 – 就像它是按钮中的未处理exception一样 -点击处理程序
另外一个问题是,如果任务被取消,我不想打电话给WaitAll
。 如果你等待一个被取消的任务,TPL会抛出一个TaskCanceledException
,重新抛出是没有意义的。
多项任务
在我的实际代码中,我有多个任务 – 初始任务和多个延续。 如果其中任何一个(可能不止一个)获得exception,我想将AggregateException
传播回UI线程。 以下是如何处理:
var synchronizationContext = SynchronizationContext.Current; var firstTask = Task.Factory.StartNew(...); var secondTask = firstTask.ContinueWith(...); var thirdTask = secondTask.ContinueWith(...); Task.Factory.ContinueWhenAll( new[] { firstTask, secondTask, thirdTask }, tasks => synchronizationContext.Post(state => Task.WaitAll(tasks.Where(task => !task.IsCanceled).ToArray()), null));
相同的故事:完成所有任务后,在任务的上下文之外调用WaitAll
。 它不会阻止,因为任务已经完成; 如果任何任务出现故障,它只是一种抛出AggregateException
的简单方法。
起初我担心,如果其中一个继续任务使用的东西像TaskContinuationOptions.OnlyOnRanToCompletion
,并且第一个任务出现故障,那么WaitAll
调用可能会挂起(因为延续任务永远不会运行,我担心WaitAll
会阻止等待它跑步)。 但事实certificate,TPL设计者比那更聪明 – 如果由于OnlyOn
或NotOn
标志而不能运行延续任务,那么延续任务将转换到Canceled
状态,因此它不会阻止WaitAll
。
编辑
当我使用多任务版本时, WaitAll
调用抛出一个AggregateException
,但是AggregateException
没有通过ThreadException
处理程序:而是只有一个内部exception传递给ThreadException
。 因此,如果多个任务抛出exception,则只有其中一个到达线程exception处理程序。 我不清楚为什么会这样,但我想弄明白。
我没有办法让这些exception像主线程中的exception那样传播。 为什么不挂钩与Application.ThreadException
挂钩的同一个处理Application.ThreadException
到TaskScheduler.UnobservedTaskException
呢?
这样的事情适合吗?
public static async void Await(this Task task, Action action = null) { await task; if (action != null) action(); } runningTask.Await();