await和async更多的理解

最近有很多網友提起await和async,呵呵,C# 5引進的語法糖。html

這個語法糖還真很差吃,能繞倒一堆初學的朋友,在網上也有不少網友關於這塊知識點的爭論,有對有錯,今天在這裏把這個誤區好好講講。web

await(C# 參考)這樣寫道

「await 運算符應用於異步方法中的任務,在方法的執行中插入掛起點,直到所等待的任務完成。 任務表示正在進行的工做。」sql


不要小看這兩句話,內容裏暗指的意思還真很多。數據庫

1)await 運算符針對於異步方法編程

2)await 插入掛起點windows

3)await 等待任務完成網絡

4)任務表式正在進行的工做多線程

帶着上面四點,咱們暫時停下,由於提到await不由要聯想到一個好基友aysncmvc

await(C# 參考)這樣寫道

"await 僅可用於由 async 關鍵字修改的異步方法中"app


到這裏,咱們對上面的四個關鍵點,提出疑問。

await 運算符針對於異步方法,那若是咱們在異步方法裏添加入同步方法會怎麼樣?

  private static async Task<TResult> XAsync()
  {
           
     X(); //X()同步方法
        
     return await XXAsync(); //XXAsync()異步方法
  }

 

而後在mainTest主線程裏調用這個XAsync()方法

static void Main(string[] args)
{
   XAsync();

OtherMethod(); }

 

 

在main方法裏,網上有網友博客說道:

 

1)XAsync在主線程裏不會被調用,直到 awiat XAsync 纔會被成功調用。就像linq to sql表達式一像,首先是var results=array.select().where();語句同樣,他們只是組裝,
並不會執行,要等到foreach(var result in results){ ...}迭代時或者.toList()再真正的查詢。

2)XAsync在主線程裏會被調用,並不阻止主線程,主線程將繼續執行下面的程序!緣由是咱們寫這個方法時候,vs會給咱們警告,有圖爲據

究竟是誰正確呢?

呵呵,其實這裏兩種說法都不正確。

首先,XAsync()在主線程裏,直接調用,確定執行,VS此時也瞎胡鬧,警告咱們說,「不等待此方法」,這是有個大大的前提!那就是這個方法體內,必須是異步的!
可能說到此很差理解。

在XAsync()方法裏,上面有一段同步方法X(),此時它是運行在主線程上的同步方法,會阻止主線程,在X()運行徹底後,在運行至 return await XXAsync()時,才把主線程調用權交還給調用的方法

在此過程裏,並不會產生新的線程,所有運行在主線程上

呵呵,越說越迷糊了。。。

await和async 講白了點,他們並不會真正意義上的去產生新的線程,咱們都知道,產生線程能夠用Task類或Thread類。
那麼async 標註的方法究竟是什麼呢?微軟給咱們的一句單簡的話," 僅可用於由 async 關鍵字修改的異步方法中"

這就說明,async是爲了await起到一種「配合」做用。而是說async修飾的方法都是異步的,那也太至關然了。

在同步方法裏,執行具備async修飾的方法,按同步方法來執行。也就是說X()是運行在主線程上的方法。

至於X()方法體內至於執行同步仍是異步決定權由方法體內的方法是否據有異步功能!
await
就像咱們上面同樣,XAsync()方法體內的X()就同步,那麼X()執行的依然是同步方法同樣,而不是運行另一線程上。

那麼問題來了,那咱們都直接X()方法多好,await還有何用,或者await 爲什麼不直接調用下X()方法呢?。。。

因而咱們繼續看下一句XXAsync()方法,咱們既然用了await 語法,爲何他能夠用?我能夠把他去了await嗎?

固然是確定的,就像這樣:

private static async Task<TResult> XAsync()
  {
           
     X(); //X()同步方法0
        
     XXAsync();//"異步方法1"

     return await XXXAsync(); //XXAsync()異步方法2
  }


XXAsync() 此時是如何運行的呢?同步仍是異步?問題又返回來了,至於同步仍是異步,不是這個方法「名詞」決定的,而是這個方法體內是如何執行的

如:


 private static async Task XXAsync()
  {  
     X();  
  }

 

此時像上面調用的方式,XXAsync()就是咱們平時的同步方法嘛!

可是改下:

private static async Task XXAsync()
  {  
     Task.Run(() =>{
             X();   
      });
  }


依據用相同的方法調用XXAsync它,這個時候,真正的運行在另外「任務上」了哦,會運行在其它線程!

寫到這裏,咱們並無和await有關哦。

那麼在XXAsync()方法前加await到底有何不一樣?

這裏首先要澄清的是:加不加 await 與 XXAsync()是異步仍是同步的並無關係!

await 真正的目的只有一個 在調用異步方法 XXAsync() 時掛起此方法,它認爲這個方法是比較耗時的方法,主線程或調用此方法的線程不要在此方法等待。
並同時做個標記,當前執行的線程運行結束時,將由此處恢復運行,因此在await 標記的異步方法下面的代碼或方法將不能運行,必須等待這個方法完成!


如:
private static async Task XAsync()
  {
           
     await  XXAsync();

     OtherMothod(); 

  }

 

在運行至 await XXAsync()時,調用方法XAsync()者將再也不運行,直接返回,就像咱們程序代碼中的return語句。這樣作的好處是,調用線程,不將等待此耗時線程。直接讓調用線程往下運行,

若是調用線程向上一直存在await 鏈,就像方法套方法裏有return同樣,會逐層向上返回,一直返回到主線程。

而每一個「子線程」去等待耗時的I/O處理,好比 操做數據庫和網絡流任何,這裏特別強調I/O處理,試想下,咱們的程序要查詢一個數據庫,可能要有點時間,此時查詢命令可能已運行在了sql數據庫裏,

若是數據庫在遠程另一臺機器上呢?咱們的"子線程或者任務「只是等待,而此時的主線程可能已完成。

如何理解主線程已完成呢?Asp.net Mvc 的機制就在這裏,咱們都知道,IIS裏的線程池是有限的,每次的Client端請求,都會到線程池裏取一個空閒的線程,若是主線程一直在」佔用「線程池,

很快線程池就會被利用完啦。此時咱們平時說的」吞吐量「的高低就是與此息息相關!當線程池被請求完後,再次有新的Client端請求,要會等待線程池的釋放。

而mvc 就引用了控制器裏異步方法的機制,原理就是讓耗時的線程,直接返回,交給主線程,從而主線程會第一時間釋放線程池的佔用,而耗時的子線程完成時,將會在await標記從繼續運行,

由此能夠看出Mvc的異步將大大提升了應用程序的」吞吐量「。

至於具體的mvc異步編程機制與原理,網上一大把,也能夠看看mvc的源代碼,這裏只簡單的說下,本文的主旨await標記給異步帶來的做用。



話題轉回來:

那麼咱們什麼時候在調用異步方法用await 做」標記「呢?

看看microsoft的經典例子

// Three things to note in the signature:  
//  - The method has an async modifier.   
//  - The return type is Task or Task<T>. (See "Return Types" section.)  
//    Here, it is Task<int> because the return statement returns an integer.  
//  - The method name ends in "Async."  
async Task<int> AccessTheWebAsync()  
{   
    // You need to add a reference to System.Net.Http to declare client.  
    HttpClient client = new HttpClient();  

    // GetStringAsync returns a Task<string>. That means that when you await the  
    // task you'll get a string (urlContents).  
    Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com");  1) // You can do work here that doesn't rely on the string from GetStringAsync.  
    DoIndependentWork();  2) // The await operator suspends AccessTheWebAsync.  
    //  - AccessTheWebAsync can't continue until getStringTask is complete.  
    //  - Meanwhile, control returns to the caller of AccessTheWebAsync.  
    //  - Control resumes here when getStringTask is complete.   
    //  - The await operator then retrieves the string result from getStringTask.  
    string urlContents = await getStringTask;  3) // The return statement specifies an integer result.  
    // Any methods that are awaiting AccessTheWebAsync retrieve the length value.  
    return urlContents.Length;  
}  

對上面的例子作了1)、2)、3)紅色標記,

在1)處定義了一個網絡流操做,認爲此方法可能會耗時,若是在此處,添加一個await client.GetStringAsync("http://msdn.microsoft.com")

對程序來講,這是一個異步操做,開闢了一個新線程,同時程序在此處返回給被調線程或UI,等網絡流返回結束時繼續在運換醒被調線程或主線程,並因爲繼續往下運行其它方法。
對於像這樣的網站,一級一級的向上await 不會形成任何的吞吐量或響應速度的下降,但是新的問題來了,接下來2)處
DoIndependentWork()方法必須等到1)完成才能繼續運行,「跳出來站在更高點看下」,這不至關於」同步「了嗎?按順序一步一步來的,子線程關沒有給咱們太多的優點。

是的,確實如此。

咱們知道,讓「子線程或任務」幹事情,主線程繼續他的活兒纔對的,因此在1)處,不該該用await,讓」線程或任務再跑一會「。
因爲咱們沒有加await,因而主線程或調用線程與調用網絡流的子線程」一塊運行「了。

當程序運行至3)時,1)標記處的任務可能已經完成或者快要完成,此時用了await目的只有一個,下面的一句話 urlContents.Length 要用到異步結果,必須待等結束,並同時向調用線程或主線程返回標記,

以使調用者最快的響應,而不是等待以致於阻塞。

回過頭來看下:咱們即要多線程或多任務執行咱們的程序,讓耗時的任務獲得執行,同時又要給調用者快速響應,無論他有沒有完成任務! 這纔是真正的目的。

再想一想咱們前面說的,DoIndependentWork()調用,加不加await,方法確定是執行的,同時與該方法異步仍是同步也沒有關係,只是要不要作」標記「而已

至於加不加標記,就是上面咱們解釋的理由,忘了,回過頭來看看吧

再來看看下面的問題:

若是一個方法裏存在多個await,會如何執行呢?

咱們能夠按照上面的猜測下,await某些時候功能很像return,多個await,相必,第一個awiat會返回,並做標記,後面的任何代碼都要等待 如:


private static async TaskXAsync()
  {
           
    await  XXAsync();

    await  XXXAsync(); 
  }

 

事實狀況確實如此,XXXAsync()必須等待XXAsync()方法執行結束!此時不會影響調用者的響應速度,但會影響咱們代碼的執行效率,這點和兩個共步方法稍有區別 

private static async TaskXAsync()
  {
           
    XX();

    XXX(); 
  }
像上面的例子XX()和XXX()兩同步方法,不只按順序執行,並且調用者也沒法拿回調用權,也就是沒法及時響應,必須待兩個方法都結束爲止。

」偷偷的想下「,我想在XX(),XXX()方法前加一個await 不就好了嗎?

回過頭來想一想,上面說過: 僅可用於由 async 關鍵字修改的異步方法中""await

實際上咱們在VS加上那麼await會報錯,編譯不過!   但願破滅。。。此時已經看出被async修飾的目的。由於XX()和XXX()並沒被修飾。

那好了,咱們就強制在同步方法上用async !

XX()
{
    code here...
}

 

實際上當咱們強制在XX()方法上加上Async時VS已經提示以下:

 

很顯然,同步方法,想提升調用者的響應速度是不可能僅僅靠async 就能完成的!根本緣由就是調用者與執行方法在同一個線程上。

 

 

 再回過頭來繼續咱們上面的例子

 

private static async TaskXAsync()
  {
           
    await  XXAsync();

    await  XXXAsync(); 
  }

上面已清楚,這兩個僅僅按順序執行,並不能並行執行,勢必影響執行效率,那麼如何才能讓他們並行執行呢?
microsoft有專門的方法 Task.WhenAll(Tasks) 咱們能夠看看microsoft的例子 如:

await SumPageSizesAsync();
 private async Task SumPageSizesAsync()
        {
            // Make a list of web addresses.
            List<string> urlList = SetUpURLList();


            // Declare an HttpClient object and increase the buffer size. The
            // default buffer size is 65,536.
            HttpClient client = new HttpClient() { MaxResponseContentBufferSize = 1000000 };

            // Create a query.
            IEnumerable<Task<int>> downloadTasksQuery = 
                from url in urlList select ProcessURL(url, client);

            // Use ToArray to execute the query and start the download tasks.
            Task<int>[] downloadTasks = downloadTasksQuery.ToArray();

            // You can do other work here before awaiting.

            // Await the completion of all the running tasks.
            int[] lengths = await Task.WhenAll(downloadTasks);

            //// The previous line is equivalent to the following two statements.
            //Task<int[]> whenAllTask = Task.WhenAll(downloadTasks);
            //int[] lengths = await whenAllTask;

            int total = lengths.Sum();

            //var total = 0;
            //foreach (var url in urlList)
            //{
            //    // GetByteArrayAsync returns a Task<T>. At completion, the task
            //    // produces a byte array.
            //    byte[] urlContent = await client.GetByteArrayAsync(url);

            //    // The previous line abbreviates the following two assignment
            //    // statements.
            //    Task<byte[]> getContentTask = client.GetByteArrayAsync(url);
            //    byte[] urlContent = await getContentTask;

            //    DisplayResults(url, urlContent);

            //    // Update the total.
            //    total += urlContent.Length;
            //}

            // Display the total count for all of the web addresses.
            resultsTextBox.Text +=
                string.Format("\r\n\r\nTotal bytes returned:  {0}\r\n", total);
        }

 

// The actions from the foreach loop are moved to this async method.
        async Task<int> ProcessURL(string url, HttpClient client)
        {
            byte[] byteArray = await client.GetByteArrayAsync(url);
            DisplayResults(url, byteArray);
            return byteArray.Length;
        }

 

  private List<string> SetUpURLList()
        {
            List<string> urls = new List<string> 
            { 
                "http://msdn.microsoft.com",
                "http://msdn.microsoft.com/en-us/library/hh290136.aspx",
                "http://msdn.microsoft.com/en-us/library/ee256749.aspx",
                "http://msdn.microsoft.com/en-us/library/hh290138.aspx",
                "http://msdn.microsoft.com/en-us/library/hh290140.aspx",
                "http://msdn.microsoft.com/en-us/library/dd470362.aspx",
                "http://msdn.microsoft.com/en-us/library/aa578028.aspx",
                "http://msdn.microsoft.com/en-us/library/ms404677.aspx",
                "http://msdn.microsoft.com/en-us/library/ff730837.aspx"
            };
            return urls;
        }

 

上面的例子很簡單了,組合任務Tasks,傳給 await Task.WhenAll(Tasks) 這樣,多個await 就並行獲得了執行。

 

從上面的例子,咱們看得出,每一個嵌套的方法裏,都是層層向上await,這就是await鏈,不只要做標記在子線程完成時,在此處」喚醒「同時達到快速響應調用線程,逐層向上返回,結果只有一個,最終讓最外層的調用者及時響應,而不用等待,就像MVC原理同樣,提升「吞吐量」。

可是中間有一個方法,沒向上await,被調用者依然是按照執行的方式決定是同步仍是異步。被調者,要是有返回值的,調用者,是沒辦法獲取到返回值的,由於,咱們並沒辦法知道,此方法是否已完成,因此,可能在之後的某段代碼中依然要用await 調用。



--------------------------------------------------------------------------------------------------------------------------------------------
經過最近兩天的朋友回覆,這篇文章確實沒有讓讀者們仔細理解,多是由於描述的太多,你們沒有抓住中心點,下面,爲了清楚的讓讀者理解,用一個示例流程圖來講明

你們都知道,點擊事件,可能涉及到I/O耗時的方法, 若是,咱們直接用同步方法調用,可能點擊事件要等許久才能反應,在此之中,咱們的事件是「假死」狀態

爲何會「假死」這個要好好想想,一個UI主線程在忙着!其它的事件又是走UI主線程,確定讓你等等!

這個時候,就有一個異步的解決思路,主線程UI遇到耗時的方法,尋找另一個線程幫忙,主線程得以繼續往下運行,主線程往下運行結束了,可能那個代忙的子線程尚未結束,

主線程不能等子線程,由於若是要繼續等待,此時若是要給主線程事件,那麼,主線程得不到及時響應,所以,主線程要快速的通知系統,我完成了,能夠繼續幹其它事件。

可是問題來了,在某個時候,之前的子線程忙完了,他要把忙完的工做結果交給之前的主線程,就得喚醒之前的「主線程」。



下面經過實例來分析。

 

線路1:調用同步方法DoWorkSync,這裏DoWorkSync 在不在主線程上執行(是否是異步方法),要進入到DoWorkSync方法體內檢查代碼才知道,因而進入到方法體內,僅僅有同步方法  
textBox2.Text = "sysnc method",因此這裏,主線程要同步完成此代碼段,在DoWorkSync同步代碼完成後,進入到線路2
線路2:代碼運行至 await DoWorkAsync(),系統開始檢查DoWorkAsync方法片段,await 標記只有在運行至異步的代碼段纔會打標記成功,假如
DoWorkAsync裏面有同步代碼,系統會先把同步代碼執行完,巧合的是,裏面確實有一個同步的代碼片斷

private async Task<string> DoWorkAsync()
        {

            DoWorkSync();

            return await Task.Run(() => { return "Done with work!"; });
        }

 

線路3:此時必須把同步方法DoWorkSync方法執行完爲止,這期間都是運行在主線程上的。

線路4:當運行至Task.Run(() => { return "Done with work!"; });時,系統認爲,此代碼片斷開啓了子線程,假如說 「Done with work!」是至關耗時的任務,並同時返回結果,

主線程應該把在此處做一個標記,之後這個子線程完成時,將由此繼續執行之後的代碼。此時是直接return了,並無下面的代碼,讀者能夠在此後繼續寫入其它方法。

既然都return了,爲啥還要用await呢?此時的await就比較聰明瞭,他執行的是掛起此行代碼,明確的告訴調用主線程不用等待,並立刻返回主線程。

這兒要着重得說下,爲何可以不用等待,由於這行代碼Task.Run()是開啓了子線程,也就是說,把任務交給了子線程,因此主線程才得以本身解脫出來,主線程要管的是子線程完成後,要提醒。固然也能夠不提醒。

主要看主線程要不要子線程的結果了。如何提醒?await做了一個標記,之後就從這兒提醒。

線路4:此時await 至關於咱們經常使用的「return」,經過await鏈路向上返回,就是咱們看到的線程4

線路5:當返回到button_click方法體內時,檢查 DoWorkAsync()片斷已檢查過了,裏面確實有異步方法,委託到了子線程,因而awiat 要在button處做個標記,之後這裏可能會有子線程返回結果給到此處,

若是不要DoworkAsync()子線程的結果,直接不用加await,那麼,能夠繼續執行下面的同步方法DoWorkSync(),而咱們這裏加了,說明,咱們對於這裏的子線程結果「很重視」,必需要拿到,才能繼續下面的其它方法。

此時有讀者會說,在此處等結果,不就阻礙了下面的代碼運行了嗎?會不會阻礙主線程?首先await 會阻止了下面的同步代碼運行,但不會影響主線的響應,由於awiat 對button_click說明了,這個方法是耗時的,

不用等待,因而buttom_click 纔會繼續向上返回await鏈,進入線路5,線路5,會對上層系統說,buttom_click 事件「暫時」已完成,可讓系統幹其它事了。

線路6:當Task.Run(() => { return "Done with work!"; });子任務完成了,它他要通知「之前的主線程」,系統會分配「之前的主線程」因而進入了線路7

線路7:很簡單,直接在button_click 裏的await處 得以喚醒,繼續執行如下的代碼,此時能夠拿到子線程的結果。

線路8:繼續執行同步方法

線路9:所有方法運行結束後,通知系統,全部任務完成。

但願讀者經過這個實例流圖有個理性的認識await。


小結:await與async並不能決定方法是同步仍是異步,而真正執行異步的仍是靠Task、異步委託或其它方式,await的主要做用是,
掛機耗時異步方法,把控制權及時的交給調用者,並在被調用者完成任務時,可以在此喚醒,並繼續執行其它方法。
本節的內容,部分例子只起到說明做用,來原於實踐的驗證,因爲時間倉促,並無提供完整的案例。
同時本節內容主要用簡單的語言來加以說明,但願能給讀者闡明原理,若是讀者但願更清楚的知道await和async,能夠查看源代碼


若是對於異步的更多瞭解請參考:

大話異步與並行(一)

大話異步與並行(二)

大話異步與並行(三)

 

本文部分實例參考 

await(C# 參考) https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/await

 

使用 Async 和 Await 的異步編程 (C#)  https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/async/index

 

Task.WhenAll 方法 (IEnumerable<Task>)  https://msdn.microsoft.com/zh-cn/library/windows/apps/hh160384(v=vs.110)

 

 

 

做者: 谷歌's谷歌's博客園
出處: http://www.cnblogs.com/laogu2/ 歡迎轉載,但任何轉載必須保留完整文章,在顯要地方顯示署名以及原文連接。如您有任何疑問或者受權方面的協商,請 給我留言
相關文章
相關標籤/搜索