When using async/await
in C#, the general rule is to avoid async void
as that's pretty much a fire and forget, rather a Task
should be used if no return value is sent from the method. Makes sense. What's strange though is that earlier in the week I was writing some unit tests for a few async
methods I wrote, and noticed that NUnit suggested to mark the async
tests as either void
or returning Task
. I then tried it, and sure enough, it worked. This seemed really strange, as how would the nunit framework be able to run the method and wait for all asynchronous operations to complete? If it returns Task, it can just await the task, and then do what it needs to do, but how can it pull it off if it returns void?
So I cracked open the source code and found it. I can reproduce it in a small sample, but I simply cannot make sense of what they're doing. I guess I don't know enough about the SynchronizationContext and how that works. Here's the code:
class Program
{
static void Main(string[] args)
{
RunVoidAsyncAndWait();
Console.WriteLine("Press any key to continue. . .");
Console.ReadKey(true);
}
private static void RunVoidAsyncAndWait()
{
var previousContext = SynchronizationContext.Current;
var currentContext = new AsyncSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(currentContext);
try
{
var myClass = new MyClass();
var method = myClass.GetType().GetMethod("AsyncMethod");
var result = method.Invoke(myClass, null);
currentContext.WaitForPendingOperationsToComplete();
}
finally
{
SynchronizationContext.SetSynchronizationContext(previousContext);
}
}
}
public class MyClass
{
public async void AsyncMethod()
{
var t = Task.Factory.StartNew(() =>
{
Thread.Sleep(1000);
Console.WriteLine("Done sleeping!");
});
await t;
Console.WriteLine("Done awaiting");
}
}
public class AsyncSynchronizationContext : SynchronizationContext
{
private int _operationCount;
private readonly AsyncOperationQueue _operations = new AsyncOperationQueue();
public override void Post(SendOrPostCallback d, object state)
{
_operations.Enqueue(new AsyncOperation(d, state));
}
public override void OperationStarted()
{
Interlocked.Increment(ref _operationCount);
base.OperationStarted();
}
public override void OperationCompleted()
{
if (Interlocked.Decrement(ref _operationCount) == 0)
_operations.MarkAsComplete();
base.OperationCompleted();
}
public void WaitForPendingOperationsToComplete()
{
_operations.InvokeAll();
}
private class AsyncOperationQueue
{
private bool _run = true;
private readonly Queue _operations = Queue.Synchronized(new Queue());
private readonly AutoResetEvent _operationsAvailable = new AutoResetEvent(false);
public void Enqueue(AsyncOperation asyncOperation)
{
_operations.Enqueue(asyncOperation);
_operationsAvailable.Set();
}
public void MarkAsComplete()
{
_run = false;
_operationsAvailable.Set();
}
public void InvokeAll()
{
while (_run)
{
InvokePendingOperations();
_operationsAvailable.WaitOne();
}
InvokePendingOperations();
}
private void InvokePendingOperations()
{
while (_operations.Count > 0)
{
AsyncOperation operation = (AsyncOperation)_operations.Dequeue();
operation.Invoke();
}
}
}
private class AsyncOperation
{
private readonly SendOrPostCallback _action;
private readonly object _state;
public AsyncOperation(SendOrPostCallback action, object state)
{
_action = action;
_state = state;
}
public void Invoke()
{
_action(_state);
}
}
}
When running the above code, you'll notice that the Done Sleeping and Done awaiting messages show up before the Press any key to continue message, which means the async method is somehow being waited on.
My question is, can someone care to explain what's happening here? What exactly is the SynchronizationContext
(I know it's used to post work from one thread to another) but I'm still confused as to how we can wait for all the work to be done. Thanks in advance!!
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:
AsyncMethod
is called, .NET first calls OperationStarted()
_operationCount
to 1. AsyncMethod
starts running (and starts the background task) await
statement, AsyncMethod
yields control as the task is not yet complete currentContext.WaitForPendingOperationsToComplete();
gets called _operationsAvailable.WaitOne();
Done sleeping!
Post()
method gets called, enqueuing a continuation that represents the remainder of the AsyncMethod
AsyncMethod
Done awaiting
AsyncMethod
finishes execution, causing .NET to call OperationComplete()
_operationCount
is decremented to 0, which marks the message loop as complete WaitForPendingOperationsToComplete
returns to the caller Press any key to continue. . .
Press any key to continue. . .
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.