How can I allow Task exceptions to propagate back to the UI thread?

后端 未结 4 795
一生所求
一生所求 2021-02-08 11:43

In TPL, if an exception is thrown by a Task, that exception is captured and stored in Task.Exception, and then follows all the rules on observed exceptions. If it\'s never obser

4条回答
  •  闹比i
    闹比i (楼主)
    2021-02-08 12:28

    Ok Joe... as promised, here's how you can generically solve this problem with a custom TaskScheduler subclass. I've tested this implementation and it works like a charm. Don't forget you can't have the debugger attached if you want to see Application.ThreadException to actually fire!!!

    The Custom TaskScheduler

    This custom TaskScheduler implementation gets tied to a specific SynchronizationContext at "birth" and will take each incoming Task that it needs to execute, chain a Continuation on to it that will only fire if the logical Task faults and, when that fires, it Posts back to the SynchronizationContext where it will throw the exception from the Task that faulted.

    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
    }
    

    Some notes/disclaimers on this implementation:

    • If you use the parameterless constructor, it will pick up the current SynchronizationContext... so if you just construct this on a WinForms thread (main form constructor, whatever) and it will work automatically. Bonus, I also have a constructor where you can explicitly pass in the SynchronizationContext that you got from somewhere else.
    • I have not provided an implementation of TryExecuteTaskInline so this implementation will just always just queue the Task to be worked on. I leave this as an excercise for the reader. It's not hard, just... not necessary to demonstrate the functionality you're asking for.
    • I'm using a simple/primitive approach to scheduling/executing the Tasks that leverages the ThreadPool. There are definitely richer implementations to be had, but again the focus of this implementation is simply about marshaling exceptions back to the "Application" thread

    Ok, now you have a couple options for using this TaskScheduler:

    Pre-configure TaskFactory Instance

    This approach allows you to setup a TaskFactory once and then any task you start with that factory instance will use the custom TaskScheduler. That would basically look something like this:

    At application startup

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

    Throughout code

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

    Explicit TaskScheduler Per-Call

    Another approach is to just create an instance of the custom TaskScheduler and then pass that into StartNew on the default TaskFactory every time you start a task.

    At application startup

    private static readonly SynchronizationContextFaultPropagatingTaskScheduler MyFaultPropagatingTaskScheduler = new SynchronizationContextFaultPropagatingTaskScheduler();
    

    Throughout code

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

提交回复
热议问题