Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is WhenAll sequential or concurrent?

Tags:

c#

async-await

Each time the below runs the exception for Mike is caught.

Is WhenAll sequential involving a continuation context between each Task or do all the Tasks get run concurrently? If it's concurrent why is Mike's exception always caught and not Mitch's. I put a delay in on Mike so as to give Mitch a chance. If it is sequential what is involved in making it concurrent? Would concurrent execution be applied when making a web request/doing file processing?

Assuming this code was more serious would this be a sensible approach to asynchrony? The scenario would be several methods - Jason, Mitch and Mike - run concurrently without blocking and continue the event handler when all done? What considerations around my naive implementation of exception handling should I be aware of? Any problems or potential problems to be aware of?

private async void button1_Click(object sender,EventArgs e)
{
    try
    {
        AsyncJason c1 = new AsyncJason();
        await c1.Hello();
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}

public class AsyncJason
{
    public AsyncJason()
    {
    }

    public async Task Hello()
    {
        var j = await GetJasonAsync();
        string[] dankeSchon = await Task.WhenAll(new Task<string>[] {GetJasonAsync(), GetMikeAsync(), GetMitchAsync()});
    }

    private async Task<string> GetJasonAsync()
    {
        var result = await Task.Run<string>(() => GetJason());
        return result;
    }

    private string GetJason()
    {
        return "Jason";
    }

     private async Task<string> GetMitchAsync()
    {
        var result = await Task.Run<string>(() => GetMitch());
        return result;
    }

    private string GetMitch()
    {
        throw new ArgumentException("Mitch is an idiot", "none");
    }

     private async Task<string> GetMikeAsync()
    {
        await Task.Delay(3000);
        var result = await Task.Run<string>(() => GetMike());
        return result;
    }

    private string GetMike()
    {
        throw new ArgumentException("Mike is an idiot", "none");
    }
}
like image 783
Jaycee Avatar asked Sep 06 '26 07:09

Jaycee


1 Answers

Is WhenAll sequential or concurrent?

The question doesn't really apply. The task for WhenAll is complete when all of the underlying tasks are complete. How it goes about doing that is its business.

When it comes to the exceptions, the Exception property of the Task contains an AggregateException that has all of the exceptions thrown by all of the underlying tasks.

When you await a task that has an aggregate exception representing multiple exceptions it will unwrap and re-throw the first exception in that list, not the AggregateException with all of the exceptions in it.

When creating the AggregateException it (apparently; I don't know if this is ever guarenteed anywhere) lists the exceptions based on the order of the tasks passed to WhenAll, rather than based on the order that those tasks completed.

If you're concerned about the lost exception then you should store the task it returns so that you can inspect all of the exceptions, or just rethrow the wrapped AggregateException, i.e.:

public async Task Hello()
{
    var j = await GetJasonAsync();
    var task = Task.WhenAll(new Task<string>[] { GetJasonAsync(), GetMikeAsync(), GetMitchAsync() });
    try
    {
        string[] dankeSchon = await task;
    }
    catch (Exception)
    {
        throw task.Exception;
    }
}

If you really want to have the exception that is hit first be the one that is re-thrown that is doable. One option is to basically re-write WhenAll to be our own version that just handles the exceptions slightly differently. Another option is to order the tasks based on the order that they will complete which we can, interestingly enough, do while still maintaining asynchrony and knowing nothing about the tasks. Here is an Order method that takes a sequence of tasks and returns a sequence of tasks representing the same operations, but ordered (in ascending order) based on completion time.

public static IEnumerable<Task<T>> Order<T>(this IEnumerable<Task<T>> tasks)
{
    var taskList = tasks.ToList();

    var taskSources = new BlockingCollection<TaskCompletionSource<T>>();

    var taskSourceList = new List<TaskCompletionSource<T>>(taskList.Count);
    foreach (var task in taskList)
    {
        var newSource = new TaskCompletionSource<T>();
        taskSources.Add(newSource);
        taskSourceList.Add(newSource);

        task.ContinueWith(t =>
        {
            var source = taskSources.Take();

            if (t.IsCanceled)
                source.TrySetCanceled();
            else if (t.IsFaulted)
                source.TrySetException(t.Exception.InnerExceptions);
            else if (t.IsCompleted)
                source.TrySetResult(t.Result);
        }, CancellationToken.None, TaskContinuationOptions.PreferFairness, TaskScheduler.Default);
    }

    return taskSourceList.Select(tcs => tcs.Task);
}

Essentially the idea here is to create a TaskCompletionSource for each task, add a continuation to each of the tasks provided to us, and then when any task is completed we mark the TaskCompletionSource that's not yet been completed to whatever the results of the just-completed task are.

Using this we can now write:

public async Task Hello()
{
    var j = await GetJasonAsync();
    var tasks = new[] { GetJasonAsync(), GetMikeAsync(), GetMitchAsync() };
    string[] dankeSchon = await Task.WhenAll(tasks.Order());
}

and the exception will be of the exception that was thrown first.

like image 127
Servy Avatar answered Sep 07 '26 20:09

Servy



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!