A SynchronizationContext allows posting work to a queue that is processed by another thread (or by a thread pool) — usually the message loop of the UI framework is used for this.
The async/await feature internally uses the current synchronization context to return to the correct thread after the task you were waiting for has completed.
The AsyncSynchronizationContext class implements its own message loop. Work that is posted to this context gets added to a queue.
When your program calls WaitForPendingOperationsToComplete();, that method runs a message loop by grabbing work from the queue and executing it.
If you set a breakpoint on Console.WriteLine("Done awaiting");, you will see that it runs on the main thread within the WaitForPendingOperationsToComplete() method.
Additionally, the async/await feature calls the OperationStarted() / OperationCompleted() methods to notify the SynchronizationContext whenever an async void method starts or finishes executing.
The AsyncSynchronizationContext uses these notifications to keep a count of the number of async methods that are running and haven’t completed yet. When this count reaches zero, the WaitForPendingOperationsToComplete() method stops running the message loop, and the control flow returns to the caller.
To view this process in the debugger, set breakpoints in the Post, OperationStarted and OperationCompleted methods of the synchronization context. Then step through the AsyncMethod call:
- When
AsyncMethodis called, .NET first callsOperationStarted()- This sets the
_operationCountto 1.
- This sets the
- Then the body of
AsyncMethodstarts running (and starts the background task) - At the
awaitstatement,AsyncMethodyields control as the task is not yet complete currentContext.WaitForPendingOperationsToComplete();gets called- No operations are available in the queue yet, so the main thread goes to sleep at
_operationsAvailable.WaitOne(); - On the background thread:
- at some point the task finishes sleeping
- Output:
Done sleeping! - the delegate finishes execution and the task gets marked as complete
- the
Post()method gets called, enqueuing a continuation that represents the remainder of theAsyncMethod
- The main thread wakes up because the queue is no longer empty
- The message loop runs the continuation, thus resuming execution of
AsyncMethod - Output:
Done awaiting AsyncMethodfinishes execution, causing .NET to callOperationComplete()- the
_operationCountis decremented to 0, which marks the message loop as complete
- the
- Control returns to the message loop
- The message loop finishes because it was marked as complete, and
WaitForPendingOperationsToCompletereturns to the caller - Output:
Press any key to continue. . .