3

Lets say i have the following code for example:

private async Task ManageClients()
{
    for (int i =0; i < listClients.Count; i++)
    {
        if (list[i] == 0)
            await DoSomethingWithClientAsync();
        else
            await DoOtherThingAsync();
    }

    DoOtherWork();
}

My questions are:

1. Will the for() continue and process other clients on the list?, or it will await untill it finishes one of the tasks.

2. Is even a good practice to use async/await inside a loop?

3. Can it be done in a better way?

I know it was a really simple example, but I'm trying to imagine what would happen if that code was a server with thousands of clients.

huysentruitw
  • 27,376
  • 9
  • 90
  • 133
Teler
  • 477
  • 6
  • 15
  • 2
    Possible duplicate of [Parallel foreach with asynchronous lambda](https://stackoverflow.com/questions/15136542/parallel-foreach-with-asynchronous-lambda) – mjwills Jul 23 '18 at 07:00
  • 1
    What do you *want* to happen? If you remember that you should only use `await` at points where *your code has nothing useful to do until whatever is on the right is finished*, does that answer your question? – Damien_The_Unbeliever Jul 23 '18 at 07:00
  • the word `await` clearly tells you that it will await till the task is finished. – slow Jul 23 '18 at 07:33

2 Answers2

4

In your code example, the code will "block" when the loop reaches await, meaning the other clients will not be processed until the first one is complete. That is because, while the code uses asynchronous calls, it was written using a synchronous logic mindset.

An asynchronous approach should look more like this:

private async Task ManageClients()
{
    var tasks = listClients.Select( client => DoSomethingWithClient() );
    await Task.WhenAll(tasks);
    DoOtherWork();
}

Notice there is only one await, which simultaneously awaits all of the clients, and allows them to complete in any order. This avoids the situation where the loop is blocked waiting for the first client.

John Wu
  • 50,556
  • 8
  • 44
  • 80
  • Also, if DoOtherwork doesn't need the results from the tasks you could put it before the await, so that the tasks and DoOtherwork run in parallel instead of sequential. – ckuri Jul 23 '18 at 07:13
  • This might change the behavior of the program if the code in DoSomethingWithClient effects the state of the program. – Magnus Jul 23 '18 at 07:19
  • This is not more async, just different. The original code is also very much async, interleaving with its caller. What is better depends very much on the environment and if the tasks are even independent. – bommelding Jul 23 '18 at 14:51
  • The thing is that the DoSomethingWithClient() is an I/O operation, so ideally I would want to send the result as soon as possible, but will also want to process lets say 10 clients I/O at the same time. Would this be a better aproach? – Teler Jul 23 '18 at 15:12
  • This approach allows all the I/O operations to start before waiting for the first one to finish. So yes, it's probably "better," if you want a shorter overall execution time. – John Wu Jul 23 '18 at 16:13
2

If a thread that is executing an async function is calling another async function, this other function is executed as if it was not async until it sees a call to a third async function. This third async function is executed also as if it was not async.

This goes on, until the thread sees an await.

Instead of really doing nothing, the thread goes up the call stack, to see if the caller was not awaiting for the result of the called function. If not, the thread continues the statements in the caller function until it sees an await. The thread goes up the call stack again to see if it can continue there.

This can be seen in the following code:

var taskDoSomething = DoSomethingAsync(...);
// because we are not awaiting, the following is done as soon as DoSomethingAsync has to await:
DoSomethingElse();
// from here we need the result from DoSomethingAsync. await for it:
var someResult = await taskDoSomething;

You can even call several sub-procedures without awaiting:

 var taskDoSomething = DoSomethingAsync(...);
 var taskDoSomethingElse = DoSomethingElseAsync(...);
 // we are here both tasks are awaiting
 DoSomethingElse();

Once you need the results of the tasks, if depends what you want to do with them. Can you continue processing if one task is completed but the other is not?

var someResult = await taskDoSomething;
ProcessResult(someResult);
var someOtherResult = await taskDoSomethingelse;
ProcessBothResults(someResult, someOtherResult);

If you need the result of all tasks before you can continue, use Task.WhenAll:

Task[] allTasks = new Task[] {taskDoSomething, taskDoSomethingElse);
await Task.WhenAll(allTasks);
var someResult = taskDoSomething.Result;
var someOtherResult = taskDoSomethingElse.Result;
ProcessBothResults(someResult, someOtherResult);

Back to your question

If you have a sequence of items where you need to start awaitable tasks, it depends on whether the tasks need the result of other tasks or not. In other words can task[2] start if task[1] has not been completed yet? Do Task[1] and Task[2] interfere with each other if they run both at the same time?

If they are independent, then start all Tasks without awaiting. Then use Task.WhenAll to wait until all are finished. The Task scheduler will take care that not to many tasks will be started at the same time. Be aware though, that starting several tasks could lead to deadlocks. Check carefully if you need critical sections

var clientTasks = new List<Task>();
foreach(var client in clients)
{
    if (list[i] == 0)
        clientTasks.Add(DoSomethingWithClientAsync());
    else
        clientTasks.Add(DoOtherThingAsync());
}
// if here: all tasks started. If desired you can do other things:
AndNowForSomethingCompletelyDifferent();

// later we need the other tasks to be finished:
var taskWaitAll = Task.WhenAll(clientTasks);

// did you notice we still did not await yet, we are still in business:
MontyPython();

// okay, done with frolicking, we need the results:
await taskWaitAll;
DoOtherWork();

This was the scenario where all Tasks where independent: no task needed the other to be completed before it could start. However if you need Task[2] to be completed before you can start Task[3] you should await:

foreach(var client in clients)
{
    if (list[i] == 0)
        await DoSomethingWithClientAsync());
    else
        await DoOtherThingAsync();
}
Harald Coppoolse
  • 28,834
  • 7
  • 67
  • 116
  • Thanks a lot for the detailed answer, it help me clear some things up. But what if DoSomethingWithClientAsync was an I/O operation and les say 10 clients need the result at the same time, would the await When.All be the best approach? – Teler Jul 23 '18 at 15:31