nunit是如何成功地等待异步void方法完成的
本文关键字:void 异步 等待 方法 何成功 成功 nunit | 更新日期: 2023-09-27 18:00:31
在C#中使用async/await
时,一般规则是避免使用async void
,因为这几乎是一场火灾,而忘记了,相反,如果方法没有发送返回值,则应该使用Task
。有道理。但奇怪的是,本周早些时候,我为我编写的几个async
方法编写了一些单元测试,并注意到NUnit建议将async
测试标记为void
或返回Task
。然后我试了一下,果然奏效了。这看起来真的很奇怪,因为nunit框架如何能够运行该方法并等待所有异步操作完成?如果它返回Task,它就可以等待任务,然后做它需要做的事情,但如果它返回void,它怎么能完成呢?
所以我破解了源代码并找到了它。我可以在一个小样本中复制它,但我根本无法理解他们在做什么。我想我对SynchronizationContext以及它是如何工作的还不够了解。这是代码:
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);
}
}
}
当运行上面的代码时,你会注意到Done Sleeping和Done waiting消息显示在按下任意键继续消息之前,这意味着异步方法正在以某种方式等待。
我的问题是,有人能解释一下这里发生了什么吗?SynchronizationContext
究竟是什么(我知道它用于将工作从一个线程发布到另一个线程),但我仍然困惑于如何等待所有工作完成。提前感谢!!
SynchronizationContext
允许将工作发布到由另一个线程(或线程池)处理的队列中——通常使用UI框架的消息循环。async
/await
功能在内部使用当前同步上下文,以便在您等待的任务完成后返回到正确的线程。
AsyncSynchronizationContext
类实现了自己的消息循环。发布到此上下文的工作将添加到队列中。当程序调用WaitForPendingOperationsToComplete();
时,该方法通过从队列中获取工作并执行它来运行消息循环。如果在Console.WriteLine("Done awaiting");
上设置断点,您将看到它在WaitForPendingOperationsToComplete()
方法内的主线程上运行。
此外,每当async void
方法开始或结束执行时,async
/await
功能调用OperationStarted()
/OperationCompleted()
方法来通知SynchronizationContext
。
AsyncSynchronizationContext
使用这些通知来统计正在运行但尚未完成的async
方法的数量。当这个计数达到零时,WaitForPendingOperationsToComplete()
方法停止运行消息循环,控制流返回给调用者。
要在调试器中查看此过程,请在同步上下文的Post
、OperationStarted
和OperationCompleted
方法中设置断点。然后逐步通过AsyncMethod
调用:
- 当调用
AsyncMethod
时,.NET首先调用OperationStarted()
- 这将
_operationCount
设置为1
- 这将
- 然后
AsyncMethod
的主体开始运行(并启动后台任务) - 在
await
语句中,由于任务尚未完成,AsyncMethod
产生控制权 currentContext.WaitForPendingOperationsToComplete();
被呼叫- 队列中还没有可用的操作,因此主线程在
_operationsAvailable.WaitOne();
处进入睡眠状态 - 在后台线程上:
- 在某个时刻,任务完成了睡眠
- 输出:
Done sleeping!
- 委托完成执行,任务被标记为完成
- 调用
Post()
方法,将表示AsyncMethod
的剩余部分的延续排入队列
- 由于队列不再为空,主线程将唤醒
- 消息循环运行continuation,从而恢复执行
AsyncMethod
- 输出:
Done awaiting
AsyncMethod
完成执行,导致.NET调用OperationComplete()
_operationCount
递减为0,这标志着消息循环完成
- 控件返回消息循环
- 消息循环结束,因为它被标记为完成,并且
WaitForPendingOperationsToComplete
返回给调用者 - 输出:
Press any key to continue. . .