最近有不少網友提起await和async,呵呵,C# 5引進的語法糖。
這個語法糖還真不好吃,能繞倒一堆初學的朋友,在網上也有很多網友關於這塊知識點的爭論,有對有錯,今天在這里把這個誤區好好講講。
在await(C# 參考)這樣寫道:
“await
運算符應用於異步方法中的任務,在方法的執行中插入掛起點,直到所等待的任務完成。 任務表示正在進行的工作。”
不要小看這兩句話,內容里暗指的意思還真不少。
1)await 運算符針對於異步方法
2)await 插入掛起點
3)await 等待任務完成
4)任務表式正在進行的工作
帶着上面四點,我們暫時停下,因為提到await不禁要聯想到一個好基友aysnc
在await(C# 參考)這樣寫道:
"await
僅可用於由 async 關鍵字修改的異步方法中"
到這里,我們對上面的四個關鍵點,提出疑問。
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 標注的方法到底是什么呢?微軟給我們的一句單簡的話,"await
僅可用於由 async 關鍵字修改的異步方法中"
這就說明,async是為了await起到一種“配合”作用。而是說async修飾的方法都是異步的,那也太相當然了。
在同步方法里,執行具有async修飾的方法,按同步方法來執行。也就是說X()是運行在主線程上的方法。
至於X()方法體內至於執行同步還是異步,決定權由方法體內的方法是否據有異步功能!
就像我們上面一樣,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 不就行了嗎?
回過頭來想想,上面說過:"await
僅可用於由 async 關鍵字修改的異步方法中"
實際上我們在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)
出處: http://www.cnblogs.com/laogu2/ 歡迎轉載,但任何轉載必須保留完整文章,在顯要地方顯示署名以及原文鏈接。如您有任何疑問或者授權方面的協商,請 給我留言。