Why doesn't this exception get thrown?

前端 未结 3 1852
醉话见心
醉话见心 2020-12-10 16:26

I use a set of tasks at times, and in order to make sure they are all awaited I use this approach:

public async Task ReleaseAsync(params Task[] TaskArray)
{
         


        
相关标签:
3条回答
  • 2020-12-10 16:41

    From the comment:

    these [tasks] were tied to managed resources and I wanted to release them when they became available instead of waiting for all of them to complete and then releasing.

    Using a helper async void method may give you the desired behavior for both removing the finished tasks from the list and immediately throwing unobserved exceptions:

    public static class TaskExt
    {
        public static async void Observe<TResult>(Task<TResult> task)
        {
            await task;
        }
    
        public static async Task<TResult> WithObservation(Task<TResult> task)
        {
            try
            {
                return await task;
            }
            catch (Exception ex)
            {
                // Handle ex
                // ...
    
                // Or, observe and re-throw
                task.Observe(); // do this if you want to throw immediately
    
                throw;
            }
        }
    }
    

    Then your code might look like this (untested):

    async void Main()
    {
        Task[] TaskArray = new Task[] { run().WithObservation() };
        var tasks = new HashSet<Task>(TaskArray);
        while (tasks.Any<Task>()) tasks.Remove(await Task.WhenAny(tasks));
    }
    

    .Observe() will re-throw the task's exception immediately "out-of-band", using SynchronizationContext.Post if the calling thread has a synchronization context, or using ThreadPool.QueueUserWorkItem otherwise. You can handle such "out-of-band" exceptions with AppDomain.CurrentDomain.UnhandledException).

    I described this in more details here:

    TAP global exception handler

    0 讨论(0)
  • 2020-12-10 16:44

    TL;DR: run() throws the exception, but you're awaiting WhenAny(), which doesn't throw an exception itself.


    The MSDN documentation for WhenAny states:

    The returned task will complete when any of the supplied tasks has completed. The returned task will always end in the RanToCompletion state with its Result set to the first task to complete. This is true even if the first task to complete ended in the Canceled or Faulted state.

    Essentially what is happening is that the task returned by WhenAny simply swallows the faulted task. It only cares about the fact that the task is finished, not that it has successfully completed. When you await the task, it simply completes without error, because it is the internal task that has faulted, and not the one you're awaiting.

    0 讨论(0)
  • 2020-12-10 16:50

    A Task not being awaited or not using its Wait() or Result() method, will swallow the exception by default. This behavior can be modified back to the way it was done in .NET 4.0 by crashing the running process once the Task was GC'd. You can set it in your app.config as follows:

    <configuration> 
        <runtime> 
            <ThrowUnobservedTaskExceptions enabled="true"/> 
        </runtime> 
    </configuration>
    

    A quote from this blog post by the Parallel Programming team in Microsoft:

    Those of you familiar with Tasks in .NET 4 will know that the TPL has the notion of “unobserved” exceptions. This is a compromise between two competing design goals in TPL: to support marshaling unhandled exceptions from the asynchronous operation to the code that consumes its completion/output, and to follow standard .NET exception escalation policies for exceptions not handled by the application’s code. Ever since .NET 2.0, exceptions that go unhandled on newly created threads, in ThreadPool work items, and the like all result in the default exception escalation behavior, which is for the process to crash. This is typically desirable, as exceptions indicate something has gone wrong, and crashing helps developers to immediately identify that the application has entered an unreliable state. Ideally, tasks would follow this same behavior. However, tasks are used to represent asynchronous operations with which code later joins, and if those asynchronous operations incur exceptions, those exceptions should be marshaled over to where the joining code is running and consuming the results of the asynchronous operation. That inherently means that TPL needs to backstop these exceptions and hold on to them until such time that they can be thrown again when the consuming code accesses the task. Since that prevents the default escalation policy, .NET 4 applied the notion of “unobserved” exceptions to complement the notion of “unhandled” exceptions. An “unobserved” exception is one that’s stored into the task but then never looked at in any way by the consuming code. There are many ways of observing the exception, including Wait()’ing on the Task, accessing a Task’s Result, looking at the Task’s Exception property, and so on. If code never observes a Task’s exception, then when the Task goes away, the TaskScheduler.UnobservedTaskException gets raised, giving the application one more opportunity to “observe” the exception. And if the exception still remains unobserved, the exception escalation policy is then enabled by the exception going unhandled on the finalizer thread.

    0 讨论(0)
提交回复
热议问题