如何允许Task异常传播回UI线程?

时间:2011-12-16 21:45:07

标签: c# task-parallel-library unhandled-exception

在TPL中,如果Task引发异常,则会捕获该异常并将其存储在Task.Exception中,然后遵循observed exceptions上的所有规则。如果它从未被观察过,它最终会在终结器线程上重新抛出并使进程崩溃。

有没有办法阻止Task捕获该异常,只是让它传播?

我感兴趣的任务已经在UI线程上运行(由TaskScheduler.FromCurrentSynchronizationContext提供),我希望异常能够逃脱,因此可以由我现有的Application.ThreadException处理程序处理。

我基本上希望Task中未处理的异常在按钮单击处理程序中表现得像未处理的异常:立即在UI线程上传播,并由ThreadException处理。

4 个答案:

答案 0 :(得分:11)

Ok Joe ...正如所承诺的,以下是使用自定义TaskScheduler子类来解决此问题的方法。我已经测试了这个实现,它就像一个魅力。 不要忘记如果您希望看到Application.ThreadException实际开火,则无法附加调试器!

Custom TaskScheduler

这个自定义的TaskScheduler实现在“诞生”时与特定的SynchronizationContext相关联,并将获取它需要执行的每个传入Task,将一个Continuation链接到它,只有在逻辑上才会触发Task错误,当它触发时,Post返回SynchronizationContext,它将从出现故障的Task中抛出异常。

public sealed class SynchronizationContextFaultPropagatingTaskScheduler : TaskScheduler
{
    #region Fields

    private SynchronizationContext synchronizationContext;
    private ConcurrentQueue<Task> taskQueue = new ConcurrentQueue<Task>();

    #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<Task> 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线程(主窗体构造函数,无论如何)上构造它,它将自动工作。奖金,我还有一个构造函数,你可以在其中显式传入你从其他地方获得的SynchronizationContext。
  • 我没有提供TryExecuteTaskInline的实现,所以这个实现只会将Task排队等待处理。我把这作为读者的练习。这并不难,只是......没有必要展示你要求的功能。
  • 我正在使用简单/原始方法来调度/执行利用ThreadPool的任务。肯定有更丰富的实现,但这个实现的重点只是关于将异常封送回“应用程序”线程

好的,现在您有几个使用此TaskScheduler的选项:

预配置TaskFactory实例

此方法允许您设置TaskFactory一次,然后您使用该工厂实例启动的任何任务都将使用自定义TaskScheduler。这基本上看起来像这样:

在应用程序启动时

private static readonly TaskFactory MyTaskFactory = new TaskFactory(new SynchronizationContextFaultPropagatingTaskScheduler());

整个代码

MyTaskFactory.StartNew(_ =>
{
    // ... task impl here ...
});

显式TaskScheduler Per-Call

另一种方法是创建自定义TaskScheduler的实例,然后在每次启动任务时将其传递到默认StartNew上的TaskFactory

在应用程序启动时

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);

答案 1 :(得分:4)

我找到了一种在某些时候能够充分发挥作用的解决方案。

单一任务

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,所以它实际上不会阻塞;它只会检查是否有异常,如果有,它会抛出。由于SynchronizationContext.Post回调直接从消息循环执行(在Task的上下文之外),因此TPL不会停止异常,并且它可以正常传播 - 就像它是一个按钮单击处理程序中的未处理异常。

另外一个问题是,如果任务被取消,我不想打电话给WaitAll。如果你等待一个被取消的任务,TPL会抛出一个TaskCanceledException,重新抛出是没有意义的。

多项任务

在我的实际代码中,我有多个任务 - 初始任务和多个延续。如果其中任何一个(可能不止一个)获得异常,我想将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的上下文之外拨打Task。它不会阻止,因为任务已经完成;如果任何任务出现故障,它只是一种简单的方法来抛出AggregateException

起初我担心,如果其中一个延续任务使用类似TaskContinuationOptions.OnlyOnRanToCompletion的内容,并且第一个任务出现故障,那么WaitAll调用可能会挂起(因为延续任务永远不会运行,并且我担心WaitAll会阻止等待它运行)。但事实证明,TPL设计者比这更聪明 - 如果由于OnlyOnNotOn标志而不能运行延续任务,那么延续任务将转换为Canceled状态,所以它不会阻止WaitAll

修改

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

答案 2 :(得分:0)

我没有办法让这些异常像主线程中的异常一样传播。为什么不挂钩同一个处理程序,你也挂钩Application.ThreadExceptionTaskScheduler.UnobservedTaskException

答案 3 :(得分:0)

这样的事情适合吗?

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

runningTask.Await();