1

I thought it might be wiser to use Task.WaitAll over multiple awaits. I looked into multiple awaits vs Task.WaitAll - equivalent? discussion and was confident that the latter would reap more benefits, but to my surprise, multiple awaits aren't doing a bad job either in terms of time consumed of all of them to complete sequentially. What could be the reason?

My code is as follows -

Stopwatch sw = Stopwatch.StartNew();
var t1 = Method1()
var t2 = Method2();
var t3 = Method3();
var t4 = Method4();
var t5 = Method5();
//note - in the real code there are like 10 await calls 
//which I changed to use like above code.

Task.WaitAll(t1,t2,t3,t4,t5);

for (int i = 0; i < 20; i++)
{
    someObject[i].prop1 = t1.Result[i];
    someObject[i].prop2 = t2.Result[i];
    someObject[i].prop3 = t3.Result[i];
    someObject[i].prop4 = t4.Result[i];
    someObject[i].prop5 = t5.Result[i];        
}

sw.Stop();
Debug.WriteLine($"Time taken using wait all concept {sw.ElapsedMilliseconds}");
// Time taken using wait all concept 1346 
// Time taken in **multiple** await is 995 ms

Update - 1 Below is the implementation of Method1(Methods2,3,4 and 5 are quite similar) hope it helps you help me.

private async Task<List<someObject>> Method1()
{
    List<List<someObject>> something = new List<List<someObject>>();
    var storedProcCriteria = new
    {
        //set up the object with parameters.
    };

    using (var db = _sqlConn.GetDbConnection())
    {
        string sql = @"someStoredProcedure";
        SqlMapper.GridReader results = await db.QueryMultipleAsync(sql, storedProcCriteria, commandType: CommandType.StoredProcedure);

        while (!results.IsConsumed)
        {
            data = results.Read<someObject>().ToList();
            something.Add(data);
        }
        return something;
    }
}

Update - 2 Actual comparison code

Stopwatch sw = Stopwatch.StartNew();

List<List<someObj1>> sth1 = await Method1()
List<List<someObj2>> sth2 = await Method1()
List<List<someObj3>> sth3 = await Method1()
List<List<someObj4>> sth4 = await Method1()
List<List<someObj5>> sth5 = await Method1()

for (int i = 0; i < claimData.Count; i++)
{
    someObject[i].prop1 = sth1[i];
    someObject[i].prop2 = sth2[i];
    someObject[i].prop3 = sth3[i];
    someObject[i].prop4 = sth4[i];
    someObject[i].prop5 = sth5[i];        
}

sw.Stop();
Debug.WriteLine($"Time taken in **multiple** await is {sw.ElapsedMilliseconds}");
// Time taken in **multiple** await is 995 ms
Vivek Shukla
  • 767
  • 6
  • 21
  • 2
    We can't guess what `Method1()` etc do. If those methods block, it doesn't matter whether you use `Task.WaitAll()` or `await Task.WhenAll()` to await them – Panagiotis Kanavos Mar 01 '23 at 15:29
  • 2
    Please add code so we can get a full [mre] (i.e. both compared cases and `MethodX` implementations). – Guru Stron Mar 01 '23 at 15:32
  • 3
    As for `I thought it might be wiser to use Task.WaitAll` not at all, quite the opposite. `WaitAll` doesn't affect how the already running tasks execute. `WaitAll` blocks the calling thread while waiting for the other tasks to complete. In a web application this means blocking a threadpool thread that could serve another request while awaiting. In a UI application the UI would freeze. – Panagiotis Kanavos Mar 01 '23 at 15:32
  • @GuruStron hope the updated question makes sense? – Vivek Shukla Mar 01 '23 at 15:36
  • 3
    @PanagiotisKanavos - so maybe use `await Task.WhenAll(t1, t2, t3...);` instead of `WaitAll`? – Vivek Shukla Mar 01 '23 at 15:38
  • @PanagiotisKanavos I hope the updated question make sense, could you please guide. – Vivek Shukla Mar 01 '23 at 15:54
  • And actual comparison code? – Guru Stron Mar 01 '23 at 16:22
  • How do you run them? Separately or one after another? – Guru Stron Mar 01 '23 at 16:44
  • 1
    Maybe when you run them in parallel (WaitAll) you are creating some sort of contention on the DB side. – Alberto Mar 01 '23 at 16:53
  • @Alberto All of the code is in a one giant method which is hooked up to an API endpoint. – Vivek Shukla Mar 01 '23 at 16:56
  • I think that the issue could be in the stored procedure that you call inside the method. In theory if tasks could be executed in parallel, invoke them together and call WaitAll could improve performance. But if the called stored procedure lock some resources on the db, then on db server the sp will be executed in sequential order, in this case I suggest to await each execution. Note that if your code is realated to web requests in ASP.NET the parallel exection of tasks in an action could increase performance for the single request, but could slow down other requests. – Roberto Ferraris Mar 01 '23 at 17:19
  • 1
    Calling a database in parallel is not usually a good idea. Databases are normally bottlenecked on IO not CPU, as they are normally coded for multi-threaded query compilation, so multi-threading it yourself doesn't help. And locking can often make things worse. Best bet is to optimize the code so you only need to do one call, and return less data. – Charlieface Mar 01 '23 at 19:21
  • @Charlieface - are you suggesting to stick to multiple `await` and reduce the number of return trips maybe from UI to API to UI. I for some reason am not happy with switching to `WhenAll` strategy as from a high level it made the situation worse than what I had hoped originally :( – Vivek Shukla Mar 01 '23 at 22:39
  • 1
    Reducing round trips will help a lot for small fast queries, less of a difference for long running ones. Like I said, databases are normally IO bottled, so your best bet is investing in faster storage (SSD/NVME) and more RAM – Charlieface Mar 02 '23 at 04:36

1 Answers1

2

If you await one call then await a second call, the first call will complete before the next call begins.

If you make multiple calls (returning tasks) then await them later, they will all run concurrently and complete at their own pace.

This simplified version of your code demonstrates that:

Stopwatch sw = Stopwatch.StartNew();

async Task Main()
{
    bool whenAll = true;

    string r1, r2;

    if (whenAll)
    {
        var t1 = Method1();
        var t2 = Method2();
        await Task.WhenAll(t1, t2);
        r1 = t1.Result;
        r2 = t2.Result;
    }
    else
    {
        r1 = await Method1();
        r2 = await Method2();
    }

    Console.WriteLine($"Results: {r1}, {r2}");
}

async Task<string> Method1()
{
    Console.WriteLine($"{sw.ElapsedMilliseconds} Starting Method1()");
    await Task.Delay(1000);
    Console.WriteLine($"{sw.ElapsedMilliseconds} Finishing Method1()");

    return "one";
}

async Task<string> Method2()
{
    Console.WriteLine($"{sw.ElapsedMilliseconds} Starting Method2()");
    await Task.Delay(2000);
    Console.WriteLine($"{sw.ElapsedMilliseconds} Finishing Method2()");

    return "two";
}

Output when using WaitAll():

1 Starting Method1()
1 Starting Method2()
1011 Finishing Method1()
2005 Finishing Method2()
Results: one, two

Output when awaiting calls sequentially:

1 Starting Method1()
1006 Finishing Method1()
1007 Starting Method2()
3010 Finishing Method2()
Results: one, two

As noted in the comments, using Task.WaitAll() will behave similarly to await Task.WhenAll(t1, t2) in terms of performance, but it will block a thread unnecessarily.

Given that you don't see a difference in your environment, that points to resource contention between your methods. In general, that could be anything with serialized access such as a lock, semaphore, etc. One of the comments mentions contention for DB resources as a reasonable candidate.

Eric J.
  • 147,927
  • 63
  • 340
  • 553
  • 1
    Thank you Eric, so the fact that I am observing multiple awaits performing faster might have nothing to do with the concept I am trying to implement, and I think I should go ahead and modify my code to use `WhenAll`. – Vivek Shukla Mar 01 '23 at 17:32
  • Correct. See my edit and one of the last comments. There's a good chance you're seeing serialized throughput because of contention for resources in the database. – Eric J. Mar 01 '23 at 17:33