如何允许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设计者比那更聪明 – 如果由于OnlyOnNotOn标志而不能运行延续任务,那么延续任务将转换到Canceled状态,因此它不会阻止WaitAll

编辑

当我使用多任务版本时, WaitAll调用抛出一个AggregateException ,但是AggregateException没有通过ThreadException处理程序:而是只有一个内部exception传递给ThreadException 。 因此,如果多个任务抛出exception,则只有其中一个到达线程exception处理程序。 我不清楚为什么会这样,但我想弄明白。

我没有办法让这些exception像主线程中的exception那样传播。 为什么不挂钩与Application.ThreadException挂钩的同一个处理Application.ThreadExceptionTaskScheduler.UnobservedTaskException呢?

这样的事情适合吗?

 public static async void Await(this Task task, Action action = null) { await task; if (action != null) action(); } runningTask.Await();