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

后端 未结 4 802
一生所求
一生所求 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条回答
  •  天涯浪人
    2021-02-08 12:29

    I found a solution that works adequately some of the time.

    Single task

    var synchronizationContext = SynchronizationContext.Current;
    var task = Task.Factory.StartNew(...);
    
    task.ContinueWith(task =>
        synchronizationContext.Post(state => {
            if (!task.IsCanceled)
                task.Wait();
        }, null));
    

    This schedules a call to task.Wait() on the UI thread. Since I don't do the Wait until I know the task is already done, it won't actually block; it will just check to see if there was an exception, and if so, it will throw. Since the SynchronizationContext.Post callback is executed straight from the message loop (outside the context of a Task), the TPL won't stop the exception, and it can propagate normally -- just as if it was an unhandled exception in a button-click handler.

    One extra wrinkle is that I don't want to call WaitAll if the task was canceled. If you wait on a canceled task, TPL throws a TaskCanceledException, which it makes no sense to re-throw.

    Multiple tasks

    In my actual code, I have multiple tasks -- an initial task and multiple continuations. If any of those (potentially more than one) get an exception, I want to propagate an AggregateException back to the UI thread. Here's how to handle that:

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

    Same story: once all the tasks have completed, call WaitAll outside the context of a Task. It won't block, since the tasks are already completed; it's just an easy way to throw an AggregateException if any of the tasks faulted.

    At first I worried that, if one of the continuation tasks used something like TaskContinuationOptions.OnlyOnRanToCompletion, and the first task faulted, then the WaitAll call might hang (since the continuation task would never run, and I worried that WaitAll would block waiting for it to run). But it turns out the TPL designers were cleverer than that -- if the continuation task won't be run because of OnlyOn or NotOn flags, that continuation task transitions to the Canceled state, so it won't block the WaitAll.

    Edit

    When I use the multiple-tasks version, the WaitAll call throws an AggregateException, but that AggregateException doesn't make it through to the ThreadException handler: instead only one of its inner exceptions gets passed to ThreadException. So if multiple tasks threw exceptions, only one of them reaches the thread-exception handler. I'm not clear on why this is, but I'm trying to figure it out.

提交回复
热议问题