多線程之旅:解讀async和await


早上無意中看到了async和await關鍵字,花了十幾分鍾看了一下msdn,大概明白了是什么一個東西,和大家分享一下。

await關鍵字的中文是期待的意思。在我們編程中想表達“我待會期待這里會有一個值,但我不是現在就要,我先去做其他事情,你完成的時候告訴我”。其實異步模式非常符合現實中場景,現實生活中還真的很少東西是同步的。等車的時候沒事干可以拿手機出來玩一下,首發下郵件,而不是直愣愣的干在那里等着車過來。

話說回來,在C# 5中借助await可以更好的輔助我們進行異步編程模式的開發,通過改變執行流程,使得異步執行看起來更像同步執行。

一個async方法里通常包含一個或多個的對應的await操作符,但如果沒有 await表達式也不會導致編譯錯誤。但如果調用一個async方 法,卻不使用await關鍵字來標記一個掛起點的話,程序將會忽略async關鍵字並以同步的方式執行。編譯器會對類似的問題發出警告。

我直接拿msdn里的代碼和圖來舉例吧。
 

    public partial class MainWindow : Window
    {
        private async void StartButton_Click(object sender, RoutedEventArgs e)
        {
            int contentLength = await AccessTheWebAsync();

            resultsTextBox.Text +=
                String.Format("\r\nLength of the downloaded string: {0}.\r\n", contentLength);
        }

        async Task<int> AccessTheWebAsync()
        { 

            HttpClient client = new HttpClient();

            Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com");

            DoIndependentWork();

            string urlContents = await getStringTask;
            return urlContents.Length;
        }


        void DoIndependentWork()
        {
            resultsTextBox.Text += "Working . . . . . . .\r\n";
        }
    }

 可以從圖中看出,和正常的執行流程是不一樣的,我們甚至還會回到“看似已經執行完的方法中”,接着await下面執行

image 

但其實這也不難理解,如果你用過yield return的話。可以看做執行到await的時候(箭頭第6步),方法就返回了。等await后面的動作執行完以后我們再回來繼續執行下面的步驟。

也就是說,await 表達式標記一個點,在該點上直到等待的異步操作完成方法才能繼續。 同時,方法掛起,並且返回到方法的調用方。

值得注意的是,返回並不意味着方法已經執行完成,也就是方法還沒退出。await 表達式中異步方法的掛起不能使該方法退出,並且 finally 塊不會運行。

但天有不測風雲,壯士有可能一去不復返,改一下StartButton_Click代碼,看下面的這個情況

1         private void StartButton_Click(object sender, RoutedEventArgs e)
2         {
3             int contentLength = AccessTheWebAsync().Result;
4 
5             resultsTextBox.Text +=
6                 String.Format("\r\nLength of the downloaded string: {0}.\r\n", contentLength);
7         }

 我們調用了task的Result,這個屬性內部會調用await的方法,也就是會阻塞線程,也就是說線程執行到第3行的時候,就會被阻塞了(阻塞線程是罪大惡極的事情,要牢記在心)。一直等待AccessTheWebAsync()方法完成。

但是AccessTheWebAsync()能完成嗎?答案是不能!因為AccessTheWebAsync方法需要依賴這個已經被阻塞的線程回去完成。好吧,一個互相等待的死鎖出現了。

 

你媽媽喊你回家吃飯,快點來接着執行await下面的代碼

 

 

我靠,我被Result定住了,沒辦法回去了。

 

。。。。。。。。





不指定SynchronizationContext
要解決這個問題,就要知道為什么會產生這種情況。

要產生這種情況需要滿足兩個條件,

1.我指定了要接下來執行下面代碼的SynchronizationContext

 2.SynchronizationContext里面包含的所有線程被阻塞

 

SynchronizationContext是什么東西呢?簡單來說就是.NET提供的一個類,能讓任務在指定的線程里面運行。詳細的話看我的這篇多線程之旅七——GUI線程模型,消息的投遞(post)與處理(IOS開發前傳)

一些SynchronizationContext只封裝了單獨的一個線程,比如UI thead, 還有一些封裝的是一組線程,比如說線程池,可以挑選任意一個線程池的線程來完成指定的任務。

所以說為啥這里悲劇了?因為UI thread所在的SynchronizationContexts只有它這么一個獨苗,一旦它被阻塞,就沒轍了。

所以解決這種問題的方式有這些,一個就是在await 指定的時候,選擇”哥不care你那個線程回來執行,反正來線程啊!“

 1         async Task<int> AccessTheWebAsync()
 2         { 
 3 
 4             HttpClient client = new HttpClient();
 5 
 6             Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com");
 7 
 8             DoIndependentWork();
 9 
10             string urlContents = await getStringTask.ConfigureAwait(false) 
;
11             return urlContents.Length;
12         }
第10行新加的.ConfigureAwait(false) 表達的就是這個意思,也就是我不在乎你接下來執行的線程是什么線程,來自哪個SynchronizationContext。默認是true,也就是我指定就要原來的那個SynchronizationContext,其他的寧可死也不要。

第二種方式就是讓它原來的SynchronizationContext本身就封裝了多個線程,這樣即使阻塞了一個線程,也可以調用其他線程來完成任務。

 
           
int contentLength = Task.run(AccessTheWebAsync()).Result;

覺得這樣寫會改變了原來的調用的話,也可以把這層封裝在AccessTheWebAsync方法里,這樣外部調用的方式可以保持不變。
 
           
        async Task<int> AccessTheWebAsync()
        { 

            return Task.Run(async ()=> {
         HttpClient client = new HttpClient();   Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com"); DoIndependentWork(); string urlContents = await getStringTask; return urlContents.Length; }) }


 

好吧,如果你十分鍾沒看懂的話,那咱們來看看好玩的yield return,再回來看看這個await。

為什么yield return返回的是IEnumerable<T>類型?就是因為IEnumerable表達的意思是“我只關心你當前的和下一個,我不管你總共有多少”,就和數學歸納法一樣,有第一個(n=1),有下一個(n+1),就能推斷出所有情況。所以當我們foreach遍歷一個IEnumerable類型的時候,無論是在foreach之前就計算好所有元素,還是調用一次MoveNext時才計算並返回一個,都符合這個意思。如果我們的輸入序列和輸出序列都是用yield return來實現的,那么就相當於構造了一條延遲執行的pipeline。恩,沒錯,和Linq的實現一樣的道理。

 

C#編譯器是通過一個狀態機來實現的,每次調用MoveNext方法return 出去之前,都修改當前的狀態,使得下次進入的時候是從其他狀態進入。

    class Foo
    {
        public IEnumerable<string> AnIterator()
        {
            yield return "1";
            yield return "2";
            yield return "3";
        }
    }

    class Program
    {
        static void Main()
        {
            var collection = new Foo();
            foreach (var s in collection.AnIterator())
            {
                Console.WriteLine(s);
            }
        }
    }


通過reflactor查看編譯器幫我們做的事情,我們每寫一個yield return就對應產生一個分支的case

        private bool MoveNext()
        {
            switch (this.<>1__state)
            {
                case 0:
                    this.<>1__state = -1;
                    this.<>2__current = "1";
                    this.<>1__state = 1; //修改當前狀態為1
                    return true;

                case 1:
                    this.<>1__state = -1;
                    this.<>2__current = "2";
                    this.<>1__state = 2; //修改當前狀態為2
                    return true;

                case 2:
                    this.<>1__state = -1;
                    this.<>2__current = "3";
                    this.<>1__state = 3; //修改當前狀態為3
                    return true;

                case 3:
                    this.<>1__state = -1;
                    break;
            }
            return false;
        }

 

 其實Jeffrey Richter以前就借助過yield return實現的狀態機來簡化異步編程,使得它看起來像同步編程。

下面的代碼yield return 1看起來是不是很像是await?

private static IEnumerator<Int32> PipeServerAsyncEnumerator(AsyncEnumerator ae) {
   // Each server object performs asynchronous operations on this pipe
   using (var pipe = new NamedPipeServerStream(
      "Echo", PipeDirection.InOut, -1, PipeTransmissionMode.Message,
      PipeOptions.Asynchronous | PipeOptions.WriteThrough)) {
      // Asynchronously accept a client connection
      pipe.BeginWaitForConnection(ae.End(), null);
      yield return 1;
// A client connected, let's accept another client var aeNewClient = new AsyncEnumerator(); aeNewClient.BeginExecute(PipeServerAsyncEnumerator(aeNewClient), aeNewClient.EndExecute); // Accept the client connection pipe.EndWaitForConnection(ae.DequeueAsyncResult()); // Asynchronously read a request from the client Byte[] data = new Byte[1000];     pipe.BeginRead(data, 0, data.Length, ae.End(), null); yield return 1;
// The client sent us a request, process it. Int32 bytesRead = pipe.EndRead(ae.DequeueAsyncResult()); // My sample server just changes all the characters to uppercase // But, you can replace this code with any compute-bound operation data = Encoding.UTF8.GetBytes( Encoding.UTF8.GetString(data, 0, bytesRead).ToUpper().ToCharArray()); // Asynchronously send the response back to the client pipe.BeginWrite(data, 0, data.Length, ae.End(), null);
    
yield return 1; // The response was sent to the client, close our side of the connection pipe.EndWrite(ae.DequeueAsyncResult()); } // Close happens in a finally block now! }

 

 終於,Jeffrey Richter在C#5里面終於用關鍵字await更好的實現了他的想法(他去年來上海宣講的時候說await是他創建的)
有興趣的話可以看看await創建的狀態機長啥樣干了什么
一段這樣的代碼,

private static async Task<String> MyMethodAsync(Int32 argument) {
   Int32 local = argument;
   try {
      Type1 result1 = await Method1Async(); for (Int32 x = 0; x < 3; x++) {
         Type2 result2 = await Method2Async();
      }
   }
   catch (Exception) {
      Console.WriteLine("Catch");
   }
   finally {
      Console.WriteLine("Finally");
  }
   return "Done";
}

 



Type1 result1 = await Method1Async();  經過編譯以后,會成這樣

// This is the state machine method itself
void IAsyncStateMachine.MoveNext() {
   String result = null;   // Task's result value
   // Compiler­inserted try block ensures the state machine’s task completes
   try {
      // Assume we're logically leaving the 'try' block
      // If 1st time in state machine method,
      // execute start of original method
      Boolean executeFinally = true;
      if (m_state == ­1) {
         m_local = m_argument;
      }
      // Try block that we had in our original code
      try {
         TaskAwaiter<Type1> awaiterType1;
         TaskAwaiter<Type2> awaiterType2;
         switch (m_state) {
            case ­1: // Start execution of code in 'try'
               // Call Method1Async and get its awaiter
               awaiterType1 = Method1Async().GetAwaiter();
               if (!awaiterType1.IsCompleted) {
                  m_state = 0;                   // 'Method1Async' is completing asynchronously
                  m_awaiterType1 = awaiterType1; // Save the awaiter for when we come back


          // Tell awaiter to call MoveNext when operation completes m_builder.AwaitUnsafeOnCompleted(ref awaiterType1, ref this); // The line above invokes awaiterType1's OnCompleted which approximately calls ContinueWith(t => MoveNext()) on the Task being awaited. executeFinally = false; // We're not logically leaving the 'try' block return; // Thread returns to caller } // 'Method1Async' completed synchronously break; case 0: // 'Method1Async' completed asynchronously awaiterType1 = m_awaiterType1; // Restore most­recent awaiter break;     case 1: // 'Method2Async' completed asynchronously awaiterType2 = m_awaiterType2; // Restore most­recent awaiter goto ForLoopEpilog;   }         //下面省略的是loop部分和異常處理部分,分開看比較容易看懂
        .....
      }

   m_builder.SetResult(result);

} 

 

恩,實現咋看上去有點復雜,不過注釋也已經寫得挺明白了,基本上關鍵就是。

 m_builder.AwaitUnsafeOnCompleted(ref awaiterType1, ref this) invokes awaiterType1's OnCompleted which approximately calls ContinueWith(t => MoveNext()) on the Task being awaited.

然后就return出去了,接着在task完成之后再跳轉回來。基本原理和上面的類似。

這里是loop部分的代碼

                  // After the first await, we capture the result & start the 'for' loop
                  m_resultType1 = awaiterType1.GetResult(); // Get awaiter's result

               ForLoopPrologue:
                  m_x = 0;          // 'for' loop initialization
                  goto ForLoopBody; // Skip to 'for' loop body

               ForLoopEpilog:
                  m_resultType2 = awaiterType2.GetResult();
                  m_x++;            // Increment x after each loop iteration
                  // Fall into the 'for' loop’s body

               ForLoopBody:
                  if (m_x < 3) {  // 'for' loop test

                     // Call Method2Async and get its awaiter
                     awaiterType2 = Method2Async().GetAwaiter();
                     if (!awaiterType2.IsCompleted) {

                        m_state = 1;                   // 'Method2Async' is completing asynchronously
                        m_awaiterType2 = awaiterType2; // Save the awaiter for when we come back

                        // Tell awaiter to call MoveNext when operation completes
                        m_builder.AwaitUnsafeOnCompleted(ref awaiterType2, ref this);
                        executeFinally = false;        // We're not logically leaving the 'try' block
                        return;                        // Thread returns to caller

                     }
                     // 'Method2Async' completed synchronously
                     goto ForLoopEpilog;  // Completed synchronously, loop around

               } 

 

 完整的代碼

void IAsyncStateMachine.MoveNext() {
   String result = null;   
   
   try {

      Boolean executeFinally = true;
      if (m_state == ­1) {
         m_local = m_argument;
      }
      
      try {
         TaskAwaiter<Type1> awaiterType1;
         TaskAwaiter<Type2> awaiterType2;
         switch (m_state) {
            case ­1: 
               
               awaiterType1 = Method1Async().GetAwaiter();
               if (!awaiterType1.IsCompleted) {
                  m_state = 0;                   
                                                 
                  m_awaiterType1 = awaiterType1; 
                  
                  m_builder.AwaitUnsafeOnCompleted(ref awaiterType1, ref this);
                  
                  executeFinally = false;        
                                                 
                  return;                        
               }
               
               break;
            case 0:  
               awaiterType1 = m_awaiterType1;  
               break;

          case 1:  
               awaiterType2 = m_awaiterType2;  
               goto ForLoopEpilog;

         }

         m_resultType1 = awaiterType1.GetResult(); 

         ForLoopPrologue:
         m_x = 0;          
         goto ForLoopBody; 

         ForLoopEpilog:
         m_resultType2 = awaiterType2.GetResult();
         m_x++;            
         

         ForLoopBody:
         if (m_x < 3) {  

            awaiterType2 = Method2Async().GetAwaiter();
            if (!awaiterType2.IsCompleted) {

               m_state = 1;                   
               m_awaiterType2 = awaiterType2; 

               
               m_builder.AwaitUnsafeOnCompleted(ref awaiterType2, ref this);
               executeFinally = false;        
               return;                        

            }
            
            goto ForLoopEpilog;  

         } 
      }

      catch (Exception) {
         Console.WriteLine("Catch");

      }
      finally {
   
         if (executeFinally) {

            Console.WriteLine("Finally");
         }

      }

      result = "Done"; 
   }

   catch (Exception exception) {
      
      m_builder.SetException(exception);
      return;

   }
   
   m_builder.SetResult(result);

} 
View Code

 

 

好吧,我承認十分鍾學會有點標題黨,但是看到這里,我想大家基本都有了個印象吧,目的達到了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM