Skip to content

Instantly share code, notes, and snippets.

@morgankenyon
Created February 2, 2020 20:27
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save morgankenyon/686b8004932be1d8e02356fb6b652cfc to your computer and use it in GitHub Desktop.
Save morgankenyon/686b8004932be1d8e02356fb6b652cfc to your computer and use it in GitHub Desktop.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace WhenAllPattern
{
class Program
{
static async Task Main(string[] args)
{
var pingTest = new PingTest();
var startTime = DateTime.UtcNow;
await pingTest.Test();
var endTime = DateTime.UtcNow;
var seconds = (endTime - startTime).TotalSeconds;
Console.WriteLine($"Seconds Elapsed: {seconds}");
}
}
public class PingTest
{
public async Task Test()
{
var pingTasks = new List<IPing>()
{
new WorkingPing(),
new BrokenPing(),
new WorkingPing(),
new BrokenPing()
};
var pingResult = new List<bool>();
var tasks = pingTasks.Select(p => BufferCall(p));
var completedPings = await Task.WhenAll(tasks);
foreach (bool ping in completedPings)
{
pingResult.Add(ping);
}
}
private async Task<bool> BufferCall(IPing ping)
{
try
{
return await ping.Ping();
}
catch (Exception e)
{
Console.WriteLine(e.Message);
return false; //or whatever default
}
}
}
public interface IPing
{
Task<bool> Ping();
}
public class WorkingPing : IPing
{
public async Task<bool> Ping()
{
await Task.Delay(3000);
return true;
}
}
public class BrokenPing : IPing
{
public async Task<bool> Ping()
{
throw new Exception("Ping Broken");
}
}
}
@karoluso
Copy link

karoluso commented Dec 12, 2021

Smart code, but isn't it that you await each task two times ? . First you await each task in BufferCall method and then if the task does not throw any exception, it is added to the list of tasks when it will be awaited again (await Task.WhenAll(tasks). Wouldn't be "lighter" to check Task.Status for any exception or add each excpetion to a list and then create Aggregated exeption basing that list. There is a lot of talk on StackOverflow on that : https://stackoverflow.com/questions/18314961/i-want-await-to-throw-aggregateexception-not-just-the-first-exception . One of the example from there is to use extension method that uses ConfigureAwait(false) . I just spent two days trying to figure out what is the best way to catch all excemptions( from all tasks). I am still a bit confused on that.

@daviddlugosz
Copy link

@karoluso I have just ran into this issue too and after debugging this solution, I can confirm that BufferCall is not triggered before WhenAll is awaited since BufferCall is not awaited in the LINQ Select

@morgankenyon
Copy link
Author

There are definitely multiple ways to handle a situation like this. So if an aggregated exception is all you need you can definitely go that approach.

When dealing with an aggregated exception, sometimes it's too late to recover on an individual level.

There are two levels of awaiting in this example yes, each is a state machine under the hood, so yes, one await would be "lighter" than 2. But in most applications:

  1. Its very normal to have many layers of awaiting,
  2. Having an extra layer of async is orders of magnitudes faster* than an actual asynchronous operation.

So I I don't think the optimization would normally be worth the effort.

  • No data to point to, but I'm pretty confident its true.

@genveir
Copy link

genveir commented Mar 1, 2023

Hello Morgan!

I'd say you don't need the double layer of awaits, or the buffered call, if you just strip away the WhenAll and await your tasks directly. Async tasks are hot from the moment they're constructed, so if you just do something like this:

// enumerate the tasks immediately
 var tasks = pingTasks.Select(p => p.Ping()).ToList();

foreach (var task in tasks)
{
    try 
    {
        // release control to the caller until the task is done, which will be near immediate for each task following the first
        pingResult.Add(await task); 
    }
    catch (Exception e)
    {
        Console.WriteLine($"Error says {e.Message}");
    }
}

your code will run in 3 seconds and you can handle all the exceptions individually. The risk is that if you forget the ToList to enumerate your tasks, the lazy enumeration will start each of them once the previous one is done.

@jag4000
Copy link

jag4000 commented Mar 30, 2024

Am I right in thinking genveir's solution runs the tasks sequentially rather than in parallel?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment