简体   繁体   中英

How to handle exceptions when using Task Parallel Library Task.WhenAny()

When I use the Task.WhenAll() function and an exception is thrown in a Task a new AggregateException is thrown, I can catch it to see all the exceptions that occurred in the Tasks. However, when I use Task.WhenAny() no exception is thrown. Instead, I have to check the Task.Exception property for a value to see if an exception occurred. This seems like a bad code smell as I would have to remember to check the Task.Exception property every I use Task.WhenAny() . Shouldn't there be a better way?

Here's an example of what I mean:

private async void btnMultipleExceptions_Click(object sender, EventArgs e) {
        var task1 = ThrowNotImplementedException();
        var task2 = ThrowDivideByZeroException();

        try {
            Task task = await Task.WhenAny(task1, task2);

            // Even if an exception is thrown in one of the tasks (in our case,
            // task1 will throw first) no exception is thrown from
            // the above await Task.WhenAny(). Instead, the exception is placed on the 
            // Task.Exception property. So I need to check for it every time 
            // I call Task.WhenAny()?
            if (task.Exception != null) {
                Console.WriteLine("Exceptions: " + string.Join(Environment.NewLine,
                    task.Exception.InnerExceptions.Select(x => x.Message).ToArray()));
            } else {
                Console.WriteLine("No Exceptions!");
            }
        } catch(Exception ex) {
            // Try to catch all exceptions???
            AggregateException allEx = ex as AggregateException;

            if (allEx != null) {
                Console.WriteLine("Exceptions: " + string.Join(Environment.NewLine,
                    allEx.InnerExceptions.Select(x => x.Message).ToArray()));
            } else {
                Console.WriteLine("Exceptions: " + ex.Message);
            }
        }
    }

    private async Task ThrowNotImplementedException() {
        await Task.Delay(TimeSpan.FromSeconds(1));
        throw new NotImplementedException();
    }

    private async Task ThrowDivideByZeroException() {
        await Task.Delay(TimeSpan.FromSeconds(2));
        throw new DivideByZeroException();
    }

Simply await the task returned from WhenAny . If it's faulted, it'll unwrap the exception and throw it, if it didn't, you know it's already done, so you can continue on.

Alternatively, you could simply call Unwrap on Task.WhenAny and then await that. This would be semantically identical to the previous option; it's only difference is in whether you think this is more or less clear.

If you find yourself very frequently unwrapping the result of WhenAny you could simply write your own implementations that unwrap the task for you, and use those instead:

public static Task WhenAny(IEnumerable<Task> tasks)
{
    return Task.WhenAny(tasks).Unwrap();
}
public static Task<T> WhenAny<T>(IEnumerable<Task<T>> tasks)
{
    return Task.WhenAny(tasks).Unwrap();
}
//TODO add wrappers for the `params` overloads if you want them too

While the accepted answer will give you the exception from the Task that completed first . The exception in the 2nd task will still be thrown, and in versions of .NET prior to 4.5 it will be escalated at the thread level when it gets GC'd if it goes unobserved. Observing and logging non-fatal task exceptions is an excellent use for Continuations (I assume from your WhenAny() scenario that it's a non-fatal condition if one of your tasks fails). Consider something like this:

private static void LogIfErrors(Task source)
{
    if(source.Exception == null) return;
    source.Exception.Handle(ex =>
    {
        Log.Error("#unhandled #task #error", ex);
        return true;
    });
    return;
}

private void DoStuff()
{
    // note that you cannot inline the ContinueWith() statement,
    // because it would change the value of task1 to hold your
    // continuation instead of your parent task

    var task1 = ThrowNotImplementedException();
    task1.ContinueWith(LogIfErrors, TaskContinuationOptions.OnlyOnFaulted);

    var task2 = ThrowDivideByZeroException();
    task2.ContinueWith(LogIfErrors, TaskContinuationOptions.OnlyOnFaulted);

    var firstCompleted = await Tasks.WhenAny(task1, task2).Unwrap();

}

Now even if one of your tasks is long-running and the exception is thrown long after your first-to-finish task has been returned by WhenAny() , you will still have the chance to observe and the exception and (in <= .NET 4.0) prevent it from killing the joined thread.

只是等待

await await Task.WhenAny(...);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM