2013-02-22 122 views
25

在C#中使用async/await时,一般的规则是避免async void,因为这几乎是一场火并忘记了,而如果没有从该方法发送返回值,应该使用Task。说得通。但奇怪的是,本周早些时候,我正在为我编写的几个async方法编写一些单元测试,并注意到NUnit建议将async测试标记为void或返回Task。然后我试了一下,果然,它工作。这看起来很奇怪,因为nunit框架如何能够运行该方法并等待所有异步操作完成?如果它返回任务,它可以等待任务,然后做它需要做的事情,但如果它返回无效,它怎么能将它关闭?nunit如何成功等待异步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); 
     } 
    } 
} 

当运行上面的代码,你会发现,睡够了,并完成等待消息显示按任意键之前继续的消息,这意味着异步方法以某种方式被侍候。

我的问题是,有人可以解释这里发生了什么? SynchronizationContext究竟是什么(我知道它用于将工作从一个线程发布到另一个线程),但我仍然对如何等待所有工作完成感到困惑。提前致谢!!

回答

24

A SynchronizationContext允许将工作发布到另一个线程(或线程池)处理的队列中 - 通常使用UI框架的消息循环。 async/await功能内部使用当前同步上下文在您等待的任务完成后返回到正确的线程。

AsyncSynchronizationContext类实现了自己的消息循环。发布到此上下文的工作将被添加到队列中。 当您的程序调用WaitForPendingOperationsToComplete();时,该方法通过从队列中抓取工作并执行它来运行消息循环。 如果您在Console.WriteLine("Done awaiting");上设置断点,您会看到它在WaitForPendingOperationsToComplete()方法中的主线程上运行。

另外,async/await特征调用OperationStarted()/OperationCompleted()方法通知SynchronizationContext每当async void方法开始或完成执行。

AsyncSynchronizationContext使用这些通知来保持正在运行且尚未完成的async方法的数量。当此计数达到零时,WaitForPendingOperationsToComplete()方法停止运行消息循环,并且控制流返回给调用者。

要在调试器中查看此过程,请在同步上下文的PostOperationStartedOperationCompleted方法中设置断点。然后通过AsyncMethod呼叫:

  • 当调用AsyncMethod时,。NET首先调用OperationStarted()
    • 这台_operationCount为1
  • 随后的AsyncMethod身体开始运行(并启动后台任务)
  • await声明,AsyncMethod收益率作为控制任务尚未完成
  • currentContext.WaitForPendingOperationsToComplete();被称为
  • 没有任何操作在队列中可用,所以主线程进入睡眠状态_operationsAvailable.WaitOne();
  • 在后台线程:
    • 在某些时候任务完成后睡觉
    • 输出:Done sleeping!
    • 委托执行完毕,任务被标记为完成
    • Post()方法被调用,入队表示余下部分的延续AsyncMethod
  • 主线程由于队列不再为空而被唤醒
  • 消息循环运行的延续,AsyncMethod
  • 输出的从而恢复执行:Done awaiting
  • AsyncMethod结束执行,导致.NET调用OperationComplete()
    • _operationCount递减到0,这标志着消息循环为完成
  • 控制返回到消息循环
  • 的乱七八糟年龄循环完成,因为它被标记为已完成,并WaitForPendingOperationsToComplete返回给调用者
  • 输出:Press any key to continue. . .
+2

稍作修改:'OperationStarted' /'OperationCompleted'只呼吁'异步void'方法,而不是'异步任务'方法。此外,'SynchronizationContext'具有事件循环以外的其他用途 - 特别是'AspNetSynchronizationContext'不是基于消息循环。我有[MSDN文章](http://msdn.microsoft.com/en-us/magazine/gg598924.aspx)更详细。 – 2013-02-22 20:13:05