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.

Was it helpful?

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 the Task 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 a Task, that means ContinueWith(_ => GetWorkAsync()) will return a Task<Task> hence the call to Unwrap() to get the 'inner task' (i.e. the actual result of GetWorkAsync()).

So:

  1. Initially there is no previous iteration, so it is simply assigned a value of Task.FromResult(_quit) - its state starts out as Task.Completed == true.
  2. The continuation is run for the first time using previous.ContinueWith(continuation)
  3. the continuation closure updates previous to reflect the completion state of _ => GetWorkAsync()
  4. When _ => GetWorkAsync() is completed, it "continues with" _previous.ContinueWith(continuation) - i.e. calling the continuation lambda again
    • Obviously at this point, previous has been updated with the state of _ => GetWorkAsync() so the continuation lambda is called when GetWorkAsync() returns.

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?)

Licensed under: CC-BY-SA with attribution
scroll top