2013-02-22 220 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