is the below code captures the exceptions from ori

2019-05-01 16:44发布

问题:

I am using TPL and async/await to build async API on top of webclient for my applications. And few of the places (generally where I need to run bunch of async tasks and wait for all of them in the end), am following code snippet. I just want to make sure I get it correct as even though writing async code is relatively easy with TPL and async/await debugging/trouble shooting is still challenging (both interactive debugging and troubleshooting issues at customer site) - so want to get it right.

My Goal: Able to capture the exception generated from the original task, continuation tasks as well as child tasks, so that I can handle it (if I need to). I don't want any exception to be fir and forgotten.

Basic principals I used: 1. .net framework ensures the exception will be attached to task 2. Try/catch block can be applied to async/await to give illusion/readability of sync code (references: http://channel9.msdn.com/Events/TechDays/Techdays-2014-the-Netherlands/Async-programming-deep-dive, http://blogs.msdn.com/b/ericlippert/archive/2010/11/19/asynchrony-in-c-5-part-seven-exceptions.aspx, http://msdn.microsoft.com/en-us/library/dd537614.aspx etc.)

Question I would like to get ratification that the desired goal (that I can capture exception from original, continuation and child tasks) has been achieved, and is there any improvements I can do to the sample:

For ex, will there be a case where one of the composed tasks (for ex, unwrapped proxy task) won't be activated at all (waitforactivation state), so that waitall may simply wait for the task to start? My understanding is that these cases should never happens as the continuation task always executes, and returns a task which has been tracked by proxy using wnwrap. As long as I follow similar pattern in all layers and apis, the pattern should capture all the aggregated exceptions in the chained tasks.

Note: essentially am looking for suggestions like avoiding dummy task am creating in continuation task if original task status is not ran to completion, or using attach to parent so that i can wait only on parent etc. to see all possibilities so that i can pick the best option as this pattern am heavily relying in my app for error handling.

static void SyncAPIMethod(string[] args)
        {
            try
            {
                List<Task> composedTasks = new List<Task>();
                //the underlying async method follow the same pattern
                //either they chain the async tasks or, uses async/await 
                //wherever possible as its easy to read and write the code
                var task = FooAsync();
                composedTasks.Add(task);
                var taskContinuation = task.ContinueWith(t =>
                    {
                        //Intentionally not using TaskContinuationOptions, so that the 
                        //continuation task always runs - so that i can capture exception
                        //in case something is wrong in the continuation
                        List<Task> childTasks = new List<Task>();
                        if (t.Status == TaskStatus.RanToCompletion)
                        {

                            for (int i = 1; i <= 5; i++)
                            {
                                var childTask = FooAsync();
                                childTasks.Add(childTask);
                            }

                        }
                        //in case of faulted, it just returns dummy task whose status is set to 
                        //'RanToCompletion'
                        Task wa = Task.WhenAll(childTasks);
                        return wa;
                    });
                composedTasks.Add(taskContinuation);
                //the unwrapped task should capture the 'aggregated' exception from childtasks
                var unwrappedProxyTask = taskContinuation.Unwrap();
                composedTasks.Add(unwrappedProxyTask);
                //waiting on all tasks, so the exception will be thrown if any of the tasks fail
                Task.WaitAll(composedTasks.ToArray());
            }
            catch (AggregateException ag)
            {
                foreach(Exception ex in ag.Flatten().InnerExceptions)
                {
                    Console.WriteLine(ex);
                    //handle it
                }
            }
        }

Best Regards.

回答1:

From the comments:

IMO, this code could could have been much more simpler and elegant with async/await. I don't understand the reason why you stick with ContinueWith and Unwrap, and why you add both the inner and the outer (unwrapped) task to composedTasks.

What I meant is something like below. I think it does the same thing as you original code, but without unnecessary redundancy in form of composedTasks, ContinueWith and Unwrap. You almost never need those if you use async/await.

static void Main(string[] args)
{
    Func<Task> doAsync = async () =>
    {
        await FooAsync().ConfigureAwait(false);

        List<Task> childTasks = new List<Task>();
        for (int i = 1; i <= 5; i++)
        {
            var childTask = FooAsync();
            childTasks.Add(childTask);
        }

        await Task.WhenAll(childTasks);
    };

    try
    {
        doAsync().Wait();
    }
    catch (AggregateException ag)
    {
        foreach (Exception ex in ag.Flatten().InnerExceptions)
        {
            Console.WriteLine(ex);
            //handle it
        }
    }
}

static async Task FooAsync()
{
    // simulate some CPU-bound work
    Thread.Sleep(1000); 
    // we could have avoided blocking like this:        
    // await Task.Run(() => Thread.Sleep(1000)).ConfigureAwait(false);

    // introduce asynchrony
    // FooAsync returns an incomplete Task to the caller here
    await Task.Delay(1000).ConfigureAwait(false);
}

Updated to address the comment:

there are some use cases where i continue after 'creating child tasks' to invoke more 'independent' tasks.

Basically, there are three common scenarios for any asynchronous task workflow: sequential composition, parallel composition, or any combination of these two (mixed composition):

  • sequential composition:

    await task1;
    await task2;
    await task3;
    
  • parallel composition:

    await Task.WhenAll(task1, task2, task3);
    
    // or
    
    await Task.WhenAny(task1, task2, task3);
    
  • mixed composition:

    var func4 = new Func<Task>(async () => { await task2; await task3; });
    await Task.WhenAll(task1, func4());
    

If any task of the above does a CPU-bound work, you can use Task.Run for that, e.g.:

    var task1 = Task.Run(() => CalcPi(numOfPiDigits));

Where CalcPi is a synchronous method doing the actual calculation.