The best practice is to collect all the async
calls in a collection inside the loop and do Task.WhenAll()
. Yet, want to understand what happens when an await
is encountered inside the loop, what would the returned Task
contain? what about further async
calls? Will it create new tasks and add them to the already returned Task
sequentially?
As per the code below
private void CallLoopAsync()
{
var loopReturnedTask = LoopAsync();
}
private async Task LoopAsync()
{
int count = 0;
while(count < 5)
{
await SomeNetworkCallAsync();
count++;
}
}
The steps I assumed are
LoopAsync
gets calledcount
is set to zero, code enters while loop, condition is checkedSomeNetworkCallAsync
is called,and the returned task is awaited- New task/awaitable is created
- New task is returned to
CallLoopAsync
()
Now, provided there is enough time for the process to live, How / In what way, will the next code lines like count++
and further SomeNetworkCallAsync
be executed?
Update - Based on Jon Hanna and Stephen Cleary:
So there is one Task and the implementation of that Task will involve 5 calls to NetworkCallAsync, but the use of a state-machine means those tasks need not be explicitly chained for this to work. This, for example, allows it to decide whether to break the looping or not based on the result of a task, and so on.
Though they are not chained, each call will wait for the previous call to complete as we have used await
(in state m/c, awaiter.GetResult();
). It behaves as if five consecutive calls have been made and they are executed one after the another (only after the previous call gets completed). If this is true, we have to be bit more careful in how we are composing the async calls.For ex:
Instead of writing
private async Task SomeWorkAsync()
{
await SomeIndependentNetworkCall();// 2 sec to complete
var result1 = await GetDataFromNetworkCallAsync(); // 2 sec to complete
await PostDataToNetworkAsync(result1); // 2 sec to complete
}
It should be written
private Task[] RefactoredSomeWorkAsync()
{
var task1 = SomeIndependentNetworkCall();// 2 sec to complete
var task2 = GetDataFromNetworkCallAsync()
.ContinueWith(result1 => PostDataToNetworkAsync(result1)).Unwrap();// 4 sec to complete
return new[] { task1, task2 };
}
So that we can say RefactoredSomeWorkAsync
is faster by 2 seconds, because of the possibility of parallelism
private async Task CallRefactoredSomeWorkAsync()
{
await Task.WhenAll(RefactoredSomeWorkAsync());//Faster, 4 sec
await SomeWorkAsync(); // Slower, 6 sec
}
Is this correct? - Yes. Along with "async all the way", "Accumulate tasks all the way" is good practice. Similar discussion is here
To produce code similar to what
async
andawait
do, if those keywords didn't exist, would require code a bit like:(The above is based on what happens when you use
async
andawait
except that the result of that uses names that cannot be valid C# class or field names, along with some extra attributes. If itsMoveNext()
reminds you of anIEnumerator
that's not entirely irrelevant, the mechanism by whichawait
andasync
produce anIAsyncStateMachine
to implement aTask
is similar in many ways to howyield
produces anIEnumerator<T>
).The result is a single Task which comes from
AsyncTaskMethodBuilder
and makes use ofLoopAsyncStateMachine
(which is close to the hiddenstruct
that theasync
produces). ItsMoveNext()
method is first called upon the task being started. It will then use an awaiter onSomeNetworkCallAsync
. If it is already completed it moves on to the next stage (incrementcount
and so on), otherwise it stores the awaiter in a field. On subsequent uses it will be called because theSomeNetworkCallAsync()
task has returned, and it will get the result (which is void in this case, but could be a value if values were returned). It then attempts further loops and again returns when it is waiting on a task that is not yet completed.When it finally reaches a
count
of 5 it callsSetResult()
on the builder, which sets the result of theTask
thatLoopAsync
had returned.So there is one
Task
and the implementation of thatTask
will involve 5 calls toNetworkCallAsync
, but the use of a state-machine means those tasks need not be explicitly chained for this to work. This, for example, allows it to decide whether to break the looping or not based on the result of a task, and so on.No. It will not. It will simply call the
async
method consequently, without storing or returning the result. The value inloopReturnedTask
will store theTask
ofLoopAsync
, not related toSomeNetworkCallAsync
.You may want to read the MSDN article on async\await.
When an
async
method first yields at anawait
, it returns aTask
(orTask<T>
). This is not the task being observed by theawait
; it is a completely different task created by theasync
method. Theasync
state machine controls the lifetime of thatTask
.One way to think of it is to consider the returned
Task
as representing the method itself. The returnedTask
will only complete when the method completes. If the method returns a value, then that value is set as the result of the task. If the method throws an exception, then that exception is captured by the state machine and placed on that task.So, there's no need for attaching continuations to the returned task. The returned task will not complete until the method is done.
I do explain this in my
async
intro post. In summary, when a methodawait
s, it captures a "current context" (SynchronizationContext.Current
unless it isnull
, in which case it usesTaskScheduler.Current
). When theawait
completes, it resumes executing itsasync
method within that context.That's what technically happens; but in the vast majority of cases, this simply means:
async
method starts on a UI thread, then it will resume on that same UI thread.async
method starts within an ASP.NET request context, then it will resume with that same request context (not necessarily on the same thread, though).async
method resumes on a thread pool thread.