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

Joe*_*ite 10 c# unhandled-exception task-parallel-library

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

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

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

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

Dre*_*rsh 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
}
Run Code Online (Sandbox Code Playgroud)

关于此实施的一些说明/免责声明:

  • 如果你使用无参数构造函数,它将获取当前的SynchronizationContext ...所以如果你只是在WinForms线程(主窗体构造函数,无论如何)上构造它,它将自动工作.额外,我还有一个构造函数,您可以在其中显式传入从其他地方获得的SynchronizationContext.
  • 我没有提供TryExecuteTaskInline的实现,所以这个实现将始终只是排队等待Task处理.我把这作为读者的练习.这并不难,只是......没有必要展示你所要求的功能.
  • 我正在使用简单/原始方法来调度/执行利用ThreadPool的Tasks.肯定有更丰富的实现,但这个实现的重点只是关于将异常封送回"应用程序"线程

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

预配置TaskFactory实例

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

在应用程序启动

private static readonly TaskFactory MyTaskFactory = new TaskFactory(new SynchronizationContextFaultPropagatingTaskScheduler());
Run Code Online (Sandbox Code Playgroud)

整个代码

MyTaskFactory.StartNew(_ =>
{
    // ... task impl here ...
});
Run Code Online (Sandbox Code Playgroud)

显式TaskScheduler Per-Call

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

在应用程序启动

private static readonly SynchronizationContextFaultPropagatingTaskScheduler MyFaultPropagatingTaskScheduler = new SynchronizationContextFaultPropagatingTaskScheduler();
Run Code Online (Sandbox Code Playgroud)

整个代码

Task.Factory.StartNew(_ =>
{
    // ... task impl here ...
},
CancellationToken.None // your specific cancellationtoken here (if any)
TaskCreationOptions.None, // your proper options here
MyFaultPropagatingTaskScheduler);
Run Code Online (Sandbox Code Playgroud)


Dre*_*rsh 0

我不知道如何让这些异常像主线程中的异常一样传播。为什么不直接挂接与您挂接的同一个处理程序Application.ThreadExceptionTaskScheduler.UnobservedTaskException