2

Note: this basically just illustrates me being confused about async. Maybe it can help other confused people.

I was doing some HttpClient async await requests in a loop, and I noticed that the requests seemed to be done sequentially (it waited for the result before starting next iteration).

So I tried a test using await Task.Delay in a loop instead of web requests.

Maybe I've lost the plot, but I would have thought that await in a loop could result in the next iteration of the loop starting while the result is awaited. Instead it seems that everything executes in sequence:

int _iterations = 1000;
int _count= 0;
Stopwatch _stopwatch = new Stopwatch();
_stopwatch.Start();                      

for (int i = 0; i < _iterations; i++)
{
    Console.WriteLine("Elapsed: {0}. Calls count: {1}.", _stopwatch.Elapsed, ++_count);
    await Task.Delay(5000);
    Console.WriteLine("Calls count again: {0}", _count);
}

This results in:

Elapsed: 00:00:00.0000059. Calls count: 1.
Calls count again: 1
Elapsed: 00:00:05.0022733. Calls count: 2.
Calls count again: 2
Elapsed: 00:00:10.0100470. Calls count: 3.
Calls count again: 3
Elapsed: 00:00:15.0182479. Calls count: 4.

Yes, I could leave out the await and add the returned Tasks to some array and use Task.WhenAll. But I am surprised that await is blocking in my loop, and that it all executes sequentially. Is this normal?

Edit: So in conclusion: I got a bit confused there. Control is returned to outside the loop at the await. This makes sense because it mimics the synchronous analogy, but of course does not block the thread.

CarenRose
  • 1,266
  • 1
  • 12
  • 24
Elliot
  • 2,002
  • 1
  • 20
  • 20

3 Answers3

3

That is expected behavior. But you are mistaken when you say that the call blocks. The UI thread is not blocked when using await - it is actually a variation of fork-and-continue. When execution hits await, you immediately return to the caller and continue after the awaited call finishes.

So, in a sense you do wait, but you do it asynchronously hence the name.

If you want to do things in parallel, use Task.WhenAll and Task.WhenAny constructs which you can then await. Example:

await Task.WhenAll(await DownloadAsync(), await DownloadAsync(), await DownloadAsync());
Toni Petrina
  • 7,014
  • 1
  • 25
  • 34
  • OK so it's not blocking the thread, and control is returning to the caller. I thought that the compiler might implement the await so that the loop continues, kicking off the next iterations rapidly. I just had to air that out; thanks for the response. Seems that I thought that control would return "to the loop" at the await, whereas it returns to outside the loop, if that makes sense. – Elliot Sep 30 '13 at 08:56
  • 1
    Your code wouldn't work. What you need is something like `await Task.WhenAll(DownloadAsync(), DownloadAsync(), DownloadAsync())`. – svick Sep 30 '13 at 10:57
  • @Elliot Well, that can be done if you don't write `await`. This will initiate the download asynchronously and ignore the result. This pattern is called fire-and-forget. However, when you do that, you lose the result since you no longer know what to do when the asynchronous operation finishes. – Toni Petrina Sep 30 '13 at 11:59
3

That's the whole point of await: that the code looks and behaves almost the same as normal synchronous code, except it's not blocking a thread. If you want to start many jobs at the same time, and then wait for them all to finish, you have to move the await after the loop.

Something like (assuming underscored identifiers are fields):

async Task PerformIteration()
{
    Console.WriteLine(
        "Elapsed: {0}. Calls count: {1}.", _stopwatch.Elapsed, ++_count);

    await Task.Delay(5000);

    Console.WriteLine("Calls count again: {0}", _count);
}

...

var tasks = new List<Task>();

for (int i=0; i<_iterations; i++)
{
    tasks.Add(PerformIteration());
}

await Task.WhenAll(tasks);

The separate method is necessary if you want to do something right after each iteration completes (in your case, the second WriteLine()). If you don't need that, you could just add the result of Task.Delay() directly to the collection of Tasks.

svick
  • 236,525
  • 50
  • 385
  • 514
  • Yep that's the way to do it. It's just that it seemed more intuitive to me if the loop kept spinning during an await inside the loop. I have to think about it more to understand why this isn't the most intuitive. – Elliot Sep 30 '13 at 11:41
2

Actually, await Task.Delay(5000) isn't blocking in your loop, Task.Delay(5000).Wait() would be. The loop will be resumed in 5 secs via a compliler-generated await continuation callback, while the method which originally called this code may resume executing as soon as you enter await here. I answered a similar question a while ago, you might find it helpful. It shows how a similar piece of code can be modelled without await.

Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • I understand that it isn't blocking the thread, that it is async because control returns to the caller that started the loop, but I wouldn't myself say that it doesn't "block the loop", since it takes 5 seconds for the next iteration to start. – Elliot Sep 30 '13 at 08:58
  • Right, `await` keeps the code flow of the method to stay logically sequential, while it may introduce parallel execution with the code which originally called that method (depending on the current synchronization context). I've corrected that phrase to match the wording at the end of your question: "blocking in the loop". – noseratio Sep 30 '13 at 09:09
  • 1
    On a side note, the code inside the loop after `await` may continue execution on a different thread (again, depending on the current thread's synchronization context). – noseratio Sep 30 '13 at 09:22