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!!
The next version of NUnit (3.0, still in alpha) will not support async void tests. So, unless you plan on staying with NUnit 2.6. 4 forever, it's probably better to always use async Task in your unit tests.
Unlike async void unit tests that are quite complicated, you can have async Task unit tests, i.e., unit tests that return a Task instance. Almost all the unit test frameworks (MSTest, NUnit, etc.) provide support for such unit tests.
The Test attribute is one way of marking a method inside a TestFixture class as a test. It is normally used for simple (non-parameterized) tests but may also be applied to parameterized tests without causing any extra test cases to be generated.
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 completecurrentContext.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 completeWaitForPendingOperationsToComplete
returns to the callerPress any key to continue. . .
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With