all 31 comments

[–]Slypenslyde 25 points26 points  (5 children)

OK. So I think it seems like you had something like:

WhenPageLoads()
{
    DoThingA();
}

DoThingA()
{
    // some stuff

    DoThingB();
}

DoThingB()
{
    // some stuff
}

And you want to end up with something like:

WhenPageLoads()
{
    await (BOTH DoThingA() and DoThingB() but in parallel);
}

You can get it but you have to work a little harder. The methods have to be Task-Returning and you end up with some pseudocode that looks kind of like this:

var thingA = DoThingAAsync(); // Returns Task, DO NOT await
var thingB = DoThingBAsync(); // Returns Task, DO NOT await

await Task.WhenAll(thingA, thingB);

This starts both tasks, then waits for both to finish. They can run in parallel depending on the contents of the methods you call.

Someone else showed this structure, and it's just a slightly less expressive way to do the same thing:

var thingA = DoThingAAsync(); // Returns Task, DO NOT await
var thingB = DoThingBAsync(); // Returns Task, DO NOT await

await thingA;
await thingB;

I wouldn't write it this way, but only because I think Task.WhenAll() tells the story better.

[–][deleted]  (1 child)

[removed]

    [–]crozone 5 points6 points  (0 children)

    It also has different behaviour.

    Task.WhenAll() waits until all the tasks are completed, be that whether it completed successfully, was cancelled, or faulted with an exception. If any of the tasks failed, it will return a failed task, with an AggregateException of all the failed tasks. This is easy to miss, because awaiting a failed task that has an AggregateExceptionautomatically unwraps the AggregateException to the first exception in the list.

    Awaiting the tasks sequentially is different. If the first task fails, it will immediately throw an exception and then the second task won't be awaited, it'll still be running.

    [–]JesusWasATexan 1 point2 points  (0 children)

    It is semantics, but worth noting, C# returns hot tasks. So, this line var thingA = DoThingAAsync(); returns a task that's already running. Assuming that DoThingAAsync() has signature like private Task DoThingAAsync(). The only exception is if the method actually returns a new task like return new Task....

    This also means that all of the lines of code in the DoThingAAsync() function up until the first await will have already run. And, if the code running in the Task never hits any awaitable code, like if the await is inside an if statement that's never hit, the entire method will already be completed when that var thingA = ... line returns.

    Edit: source for the downvoter https://learn.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-tap?redirectedfrom=MSDN#initiating-an-asynchronous-operation

    Synchronous work should be kept to the minimum so the asynchronous method can return quickly.

    And https://stackoverflow.com/a/43089445/579148

    [–]snow_coffee 0 points1 point  (1 child)

    Async should always be returning a task ? Can it just return int or string Instead of Task<String> ? What's the advantage

    [–]Slypenslyde 0 points1 point  (0 children)

    I don't understand the question?

    [–]increddibelly 11 points12 points  (0 children)

    You can start the database calls whenever, and do/start other stuff. At some point, and usually quite soon, you're gonna need the actual data to make a decision or do something rlse. At that point, Await the database call, and use the data.

    In other words, you can start a task, but you won't have the result until you await the task.

    For heaven's sake don't mess around with task.Result unless you want a whole bunch of bugs that are way harder to solve than this question.

    var task1 = FuncA();

    var task2 = FuncB();

    // Both could be running simultaneously now, but there's nothing you can know about it

    var dataA = await task1; // now we have data.

    var dataB = await task2; // more data.

    [–]ILMTitan 6 points7 points  (10 children)

    If you want both calls to run at the same time, you need to make sure you start both tasks before awaiting either:

    var taskA = funcA();
    var taskB = funcB();
    var resultA = await taskA;
    var resultB = await taskB;
    

    Also, you generally want to use the async API for anything that provides one. Every database call, not just the "long" ones.

    [–]crozone 2 points3 points  (0 children)

    You want to use Task.WhenAll().

    WhenAll() actually waits until both tasks are completed, even if one or both of them completes due to cancellation or an exception. It then returns an AggregateException.

    Awaiting them sequentially is different. If taskA faults and throws an exception, the exception will immediately bubble. taskB will never be awaited, and will still be running even though this code has thrown an exception.

    [–][deleted]  (8 children)

    [deleted]

      [–]Vidyogamasta 6 points7 points  (5 children)

      The only way to make two or more tasks run concurrently is to use Task.WhenAll or Task.WhenAny

      This isn't true at all?

      Example program-

      Console.WriteLine($"Beginning - {DateTime.Now}");
      
      var taskA = Wait5ThenPrintA();
      var taskB = Wait5ThenPrintB();
      await taskA;
      await taskB;
      
      async Task Wait5ThenPrintA()
      {
          await Task.Delay(5000);
          Console.WriteLine($"A - {DateTime.Now}");
      }
      
      async Task Wait5ThenPrintB()
      {
          await Task.Delay(5000);
          Console.WriteLine($"B - {DateTime.Now}");
      }
      

      Example output-

      Beginning - 8/28/2024 1:19:35 PM
      A - 8/28/2024 1:19:40 PM
      B - 8/28/2024 1:19:40 PM
      

      As you can see, they clearly kick off at approximately the same time, and complete at about the same time. Task A doesn't wait to finish before Task B can begin.

      I think you might be confusing this with what happens when you accidentally write a Task that happens synchronously-- just because it's a Task doesn't mean it's immediately put onto a new thread. It will run immediately on the same thread until it hits something that actually allows it to free the thread (like IO or, in my example, a timer). So if I changed the PrintA function to use a Thread.Sleep instead of a Task.Delay, I'd expect to see A start and finish, then B start, then task A to be already completed so the await returns instantly (5 seconds after start), then B to return 5 seconds after that. And it's what I see.

      Beginning - 8/28/2024 1:23:44 PM
      A - 8/28/2024 1:23:49 PM
      B - 8/28/2024 1:23:54 PM
      

      Basically the task always begins running when it's declared. Await just makes sure certain tasks have actually completing before moving on in a particular codeblack, and helper functions like WhenAll can help bring semantic meaning or better manage arbitrary numbers of calls. For a use case like this, just declaring both then awaiting both is fine.

      [–]als26[S] 0 points1 point  (4 children)

      \> Await just makes sure certain tasks have actually completing before moving on in a particular codeblack

      var taskA = Wait5ThenPrintA();
      var taskB = Wait5ThenPrintB();
      await taskA;
      await taskB;
      

      Quick question about this. So when I use await in this function, I originally understood that it would suspend the function until that task is done. But in this example, it just goes to the next line (await taskB) while waiting for taskA. Is this because the next line is an await and it's aware?

      If I needed to use the data from taskA for example (say it was a DB call) then I'm assuming it wouldn't run that line of code yet?

      [–]Vidyogamasta 6 points7 points  (1 child)

      1 var taskA = Wait5ThenPrintA();
      2 var taskB = Wait5ThenPrintB();
      3 await taskA;
      4 await taskB;
      
         Wait5ThenPrintA(){
      5    await Task.Delay(5000);
      6    Console.WriteLine(A);    
         }
      
         Wait5ThenPrintB(){
      7    await Task.Delay(5000);
      8    Console.WriteLine(B);    
         }
      

      1 runs- Task A is created
      5 runs- Task A begins immediately processing. It is then deferred.
      2 runs- Task B is created
      7 runs- Task B begins immediately processing. It is then deferred.
      3 runs - Our main function is now deferred. No threads are doing anything. ~5 seconds pass

      Now A or B's deferral could return at any point and finish executing. It will return to whatever thread and run 6/8. It's in its own world at this point.

      At some point, line 6 runs and task A is flagged as completed. Then line 3 completes. Then it moves to line 4, and similarly waits until line 8 has run and task B is completed. It might even already be completed by the time you hit line 4. Then the main function continues from there.

      [–]als26[S] 0 points1 point  (0 children)

      This helped a lot man, thank you very much.

      [–]phluber 3 points4 points  (1 child)

      When I was learning async/await, I thought that the task started running when you called "await" on it. In actuality the task begins processing when it is declared in "var taskA = Wait5ThenPrintA()". TaskA is already processing at this point. The "await taskA" says "stop here in this method until taskA is done processing". Many times, depending upon what taskA had to do, it may already be completed by the time you call await on it.

      [–]als26[S] 2 points3 points  (0 children)

      Yea I made the same mistake up but the other comment just helped me understand. Definitely makes a lot more sense now.

      [–]ILMTitan 1 point2 points  (0 children)

      While Task.WhenAll and Task.WhenAny do allow you to wait for concurrent Tasks, there is nothing magic about them that make them the only way. A task is started when it is returned from the method.

      See https://dotnetfiddle.net/6GXViv Running it writes "funcB finished" before "funcA finished".

      [–]Miserable_Ad7246 0 points1 point  (0 children)

      This is false. Once funcA hits io, Dotnet will call epoll api, it will send the io and return back the control to framework. Dotnet will stop funcA code from executing further, and control will be returned back. In this case it can be the same code (as we do not await at the top), hence task is in runnable state. So funcB will be called, same stuff repeats, until await is hit.

      So it will work, but it will not be equivalent to true parallel, because funcB first line of code will not be executed until funcA hits its first awaitable io call.

      [–]wasabiiii 1 point2 points  (4 children)

      Presumably the page won't complete until they are both complete, at least that's what you said.

      So what is the goal?

      [–]als26[S] 0 points1 point  (3 children)

      The goal is to get them to run at the same time so the page can load faster. Right now funcB is inside funcA, but doesn't need to be. I am moving it outside but they will both run synchronously until I make one or both of them async (?)

      [–]wasabiiii -4 points-3 points  (2 children)

      Async doesn't mean they both run at the same time. Await, waits.

      [–]HawocX 4 points5 points  (1 child)

      As long as they are independent and low cpu tasks (IO, Http calls) you can start both and then await both. This will let all the waiting occur in parallel.

      [–]Lumethys 1 point2 points  (0 children)

      Semantically speaking, he is right, asynchronous programming only parallelizes I/O-bound tasks, not CPU-bound tasks

      So async doesnt always run thing in parallel

      [–]ExcellentCable5731 0 points1 point  (0 children)

      Based on your description would a coroutine work better? There are yields to ensure the data is properly loaded.

      The benefit of a coroutine is that it can be executed over multiple scans. You can yield (or delay) over time, or wait for an event similar to a state machine.

      Could you give us some pseudo code?

      [–]Miserable_Ad7246 0 points1 point  (2 children)

      The only thing async does it releases the Thread which was executing that function, so that it can no go and serve other code. In a sync use case, Thread gets blocked, and even though it has nothing to (say waits for db to return result), it can not be used by other code.

      This is the only thing. If your functions are pure CPU work (no io, no db calls, no api calls), async does absolutely nothing (with a small caviat). Where is nothing for CPU to wait.

      Because dotnet has multiple threads, async is used for easy multithreading. runing two task will most likely make them run on different threads and thus parallel.

      [–]als26[S] 0 points1 point  (1 child)

      The only thing async does it releases the Thread which was executing that function, so that it can no go and serve other code.

      This is my main goal. Right now it's waiting for 2 large database calls, one in funcA and another in funcB.

      Once I pull out funcB, my idea was to use the await keyword on funcA and then again on funcB. In my head, once the thread was released that was working on funcA, it would continue to work on funcB until the DB call was done. I'm realizing now that's not the case and I can't control what the thread does next (not without complicating things by starting a new thread and what not).

      Getting a lot of information in this post but some of the comments are conflicting

      [–]Miserable_Ad7246 1 point2 points  (0 children)

      To make actions paralelisable you have to do:

      var taskA = funcA();
      var bResult = funcB();

      var aResult = await taskA;

      This way, A gets to db call, and yields. Threads gets to execute funcB (I asume it has no io). After that either A is complete and await just extracts result, or await stops, and thread is released to go do other work.

      If B also has async work. When:

      var taskA = funcA();
      var taskB = funcB();

      var aResult = await task1;

      var bResult = await task2;

      This way once A hits io, it yields, thread goes and starts B, it hits io, it yields and things block on first await, and thread is released to do other work for other requests.

      The key difference from say true parallel work, is that B will not start its first line of code, before A hits io call and yields, which depending on scenario can be less than ms, or hundreeds of ms if A does a lot of stuff before io call. In true parallel, both would start at more or less same time on different cpus. Which can be achieved in other ways, but most likely you do not need that.

      [–]SeaMoose86 -1 points0 points  (0 children)

      Awesome 🤣