What exactly happens when a thread awaits a task inside a while loop?
https://softwareengineering.stackexchange.com/questions/342547
Question
After dealing with C#'s async/await pattern for a while now, I suddenly came to realization that I don't really know how to explain what happens in the following code:
async void MyThread()
{
while (!_quit)
{
await GetWorkAsync();
}
}
GetWorkAsync()
is assumed to return an awaitable Task
which may or may not cause a thread switch when the continuation is executed.
I wouldn't be confused if the await wasn't inside a loop. I'd naturally expect that the rest of the method (i.e. continuation) would potentially execute on another thread, which is fine.
However, inside a loop, the concept of "the rest of the method" gets a bit foggy to me.
What happens to "the rest of the loop" if the thread is switched on continuation vs. if it isn't switched? On which thread is the next iteration of the loop executed?
My observations show (not conclusively verified) that each iteration starts on the same thread (the original one) while the continuation executes on another. Can this really be? If yes, is this then a degree of unexpected parallelism that needs to be accounted for vis-a-vis thread-safety of the GetWorkAsync method?
UPDATE: My question is not a duplicate, as suggested by some. The while (!_quit) { ... }
code pattern is merely a simplification of my actual code. In reality, my thread is a long-lived loop that processes its input queue of work items on regular intervals (every 5 seconds by default). The actual quit condition check is also not a simple field check as suggested by the sample code, but rather an event handle check.
Solution
You can actually check it out at Try Roslyn. Your await method gets rewritten into void IAsyncStateMachine.MoveNext()
on the generated async class.
What you'll see is something like this:
if (this.state != 0)
goto label_2;
//set up the state machine here
label_1:
taskAwaiter.GetResult();
taskAwaiter = default(TaskAwaiter);
label_2:
if (!OuterClass._quit)
{
taskAwaiter = GetWorkAsync().GetAwaiter();
//state machine stuff here
}
goto label_1;
Basically, it doesn't matter which thread you're on; the state machine can resume properly by replacing your loop with an equivalent if/goto structure.
Having said that, async methods don't necessarily execute on a different thread. See Eric Lippert's explanation "It's not magic" to explain how you can have working async/await
on only one thread.
OTHER TIPS
Firstly, Servy has written some code in an answer to a similar question, upon which this answer is based:
https://stackoverflow.com/questions/22049339/how-to-create-a-cancellable-task-loop
Servy's answer includes a similar ContinueWith()
loop using TPL constructs without explicit use of the async
and await
keywords; so to answer your question, consider what your code might look when your loop is unrolled using ContinueWith()
private static Task GetWorkWhileNotQuit()
{
var tcs = new TaskCompletionSource<bool>();
Task previous = Task.FromResult(_quit);
Action<Task> continuation = null;
continuation = t =>
{
if (!_quit)
{
previous = previous.ContinueWith(_ => GetWorkAsync())
.Unwrap()
.ContinueWith(_ => previous.ContinueWith(continuation));
}
else
{
tcs.SetResult(_quit);
}
};
previous.ContinueWith(continuation);
return tcs.Task;
}
This takes some time to wrap your head around, but in summary:
continuation
represents a closure for the "current iteration"previous
represents theTask
containing the state of the "previous iteration" (i.e. it knows when the 'iteration' is finished and is used for starting the next one..)- Assuming
GetWorkAsync()
returns aTask
, that meansContinueWith(_ => GetWorkAsync())
will return aTask<Task>
hence the call toUnwrap()
to get the 'inner task' (i.e. the actual result ofGetWorkAsync()
).
So:
- Initially there is no previous iteration, so it is simply assigned a value of
Task.FromResult(_quit)
- its state starts out asTask.Completed == true
. - The
continuation
is run for the first time usingprevious.ContinueWith(continuation)
- the
continuation
closure updatesprevious
to reflect the completion state of_ => GetWorkAsync()
- When
_ => GetWorkAsync()
is completed, it "continues with"_previous.ContinueWith(continuation)
- i.e. calling thecontinuation
lambda again- Obviously at this point,
previous
has been updated with the state of_ => GetWorkAsync()
so thecontinuation
lambda is called whenGetWorkAsync()
returns.
- Obviously at this point,
The continuation
lambda always checks the state of _quit
so, if _quit == false
then there are no more continuations, and the TaskCompletionSource
gets set to the value of _quit
, and everything is completed.
As for your observation regarding the continuation being executed in a different thread, that is not something which the async
/await
keyword would do for you, as per this blog "Tasks are (still) not threads and async is not parallel". - https://blogs.msdn.microsoft.com/benwilli/2015/09/10/tasks-are-still-not-threads-and-async-is-not-parallel/
I'd suggest it is indeed worth taking a closer look at your GetWorkAsync()
method with regards to threading and thread safety. If your diagnostics are revealing that it has been executing on a different thread as a consequence of your repeated async/await code, then something within or related to that method must be causing a new thread to be created elsewhere. (If this is unexpected, perhaps there's a .ConfigureAwait
somewhere?)