溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何解析異步編程In .NET APM/EAP和async/await

發(fā)布時間:2021-10-29 17:57:39 來源:億速云 閱讀:101 作者:柒染 欄目:編程語言

如何解析異步編程In .NET APM/EAP和async/await,相信很多沒有經(jīng)驗的人對此束手無策,為此本文總結(jié)了問題出現(xiàn)的原因和解決方法,通過這篇文章希望你能解決這個問題。

概述

在之前寫的一篇關(guān)于async和await的前世今生的文章之后,大家似乎在async和await提高網(wǎng)站處理能力方面還有一些疑問,博客園本身也做了不少的嘗試。今天我們再來回答一下這個問題,同時我們會做一個async和await在WinForm中的嘗試,并且對比在4.5之前的異步編程模式APM/EAP和async/await的區(qū)別,***我們還會探討在不同線程之間交互的問題。

IIS存在著處理能力的問題,但是WinForm卻是UI響應(yīng)的問題,并且WinForm的UI線程至始至終都是同一個,所以兩者之間有一定的區(qū)別。有人會問,現(xiàn)在還有人寫WinForm嗎?好吧,它確是一個比較老的東西呢,不如WPF炫,技術(shù)也不如WPF先進(jìn),但是從架構(gòu)層面來講,不管是Web,還是WinForm,又或是WPF,Mobile,這些都只是表現(xiàn)層,不是么?現(xiàn)在的大型系統(tǒng)一般桌面客戶端,Web端,手機(jī),平板端都會涉及,這也是為什么會有應(yīng)用層,服務(wù)層的存在。我們在這談?wù)摰腁SP.NET MVC,WinForm,WFP,Android/IOS/WP 都是表現(xiàn)層,在表現(xiàn)層我們應(yīng)該只處理與“表現(xiàn)”相關(guān)的邏輯,任何與業(yè)務(wù)相關(guān)的邏輯應(yīng)該都是放在下層處理的。關(guān)于架構(gòu)的問題,我們后面再慢慢深入,另外別說我沒有提示您,我們今天還會看到.NET中另一個已經(jīng)老去的技術(shù)Web Service。

還得提示您,文章內(nèi)容有點長,涉及的知識點比較多,所以,我推薦:”先頂后看“ ,先頂后看是21世紀(jì)看長篇的***之道,是良好溝通的開端,想知道是什么會讓你與眾不同嗎?想知道為什么上海今天會下這么大的雨么?請記住先頂后看,你頂?shù)牟皇俏业奈恼拢俏覀兠爸笥赀€要去上班的可貴精神!先頂后看,你值得擁有!

async/await如何提升IIS處理能力

首先響應(yīng)能力并不完全是說我們程序性能的問題,有時候可能你的程序沒有任何問題,而且精心經(jīng)過優(yōu)化,可是響應(yīng)能力還是沒有上去,網(wǎng)站性能分析是一個復(fù)雜的活,有時候只能靠經(jīng)驗和不斷的嘗試才能達(dá)到比較好的效果。當(dāng)然我們今天討論的主要是IIS的處理能力,或者也可能說是IIS的性能,但絕非代碼本身的性能。即使async/await能夠提高IIS的處理能力,但是對于用戶來說整個頁面從發(fā)起請求到頁面渲染完成的這些時間,是不會因為我們加了async/await之后產(chǎn)生多大變化的。

另外異步的ASP.NET并非只有async/await才可以做的,ASP.NET在Web Form時代就已經(jīng)有異步Page了,包括ASP.NET MVC不是也有異步的Controller么?async/await 很新,很酷,但是它也只是在原有一技術(shù)基礎(chǔ)上做了一些改進(jìn),讓程序員們寫起異步代碼來更容易了。大家常說微軟喜歡新瓶裝舊酒,至少我們要看到這個新瓶給我們帶來了什么,不管是任何產(chǎn)品,都不可能一開始就很***,所以不斷的迭代更新,也可以說是一種正確做事的方式。

ASP.NET并行處理的步驟

ASP.NET是如何在IIS中工作的一文已經(jīng)很詳細(xì)的介紹了一個請求是如何從客戶端到服務(wù)器的HTTP.SYS***進(jìn)入CLR進(jìn)行處理的(強(qiáng)烈建議不了解這一塊的同學(xué)先看這篇文章,有助于你理解本小節(jié)),但是所有的步驟都是基于一個線程的假設(shè)下進(jìn)行的。IIS本身就是一個多線程的工作環(huán)境,如果我們從多線程的視角來看會發(fā)生什么變化呢?我們首先來看一下下面這張圖。注意:我們下面的步驟是建立在IIS7.0以后的集成模式基礎(chǔ)之上的。

如何解析異步編程In .NET APM/EAP和async/await

我們再來梳理一下上面的步驟:

  1. 所有的請求最開始是由HTTP.SYS接收的,HTTP.SYS內(nèi)部有一個隊列維護(hù)著這些請求,這個隊列的request的數(shù)量大于一定數(shù)量(默認(rèn)是1000)的時候,HTTP.SYS就會直接返回503狀態(tài)(服務(wù)器忙),這是我們的***個閥門。

  2. HTTP.SYS把請求交給CLR 線程池中的IO線程

  3. CLR 線程池中的 Worker線程從IO線程中接過請求來處理,IO線程不必等待該請求的處理結(jié)果,而是可以返回繼續(xù)去處理HTTP.SYS隊列中的請求。IO線程和Worker線程的數(shù)量上限是第二個閥門。

  4. 當(dāng)CLR中正在被處理的請求數(shù)據(jù)大于一定值(***并行處理請求數(shù)量)的時候,從IO線程過來的請求就不會直接交給Worker線程,而是放到一個進(jìn)程池級別的一個隊列了,等到這個數(shù)量小于臨界值的時候,才會把它再次交給Worker線程去處理。這是我們的第三個閥門。

哪些因素會控制我們的響應(yīng)能力

從上面我們提到了幾大閥門中,我們可以得出下面的幾個數(shù)字控制或者說影響著我們的響應(yīng)能力。

  1. HTTP.SYS隊列的長度

  2. ***IO線程數(shù)量和***Worker線程數(shù)量

  3. ***并行處理請求數(shù)量

HTTP.SYS隊列的長度

這個我覺得不需要額外解釋,默認(rèn)值是1000。這個值取決于我們我們后面CLR IO線程和Worker線程的處理速度,如果它們兩個都處理不了,這個數(shù)字再大也沒有用。因為***他們會被存儲到進(jìn)程池級別的隊列中,所以只會造成內(nèi)存的浪費。

***IO線程數(shù)量和***Worker線程數(shù)量

這兩個值是可以在web.config中進(jìn)行配置的。

如何解析異步編程In .NET APM/EAP和async/await

maxIoThreads: 從HTTP.SYS隊列中拿請求的***IO線程數(shù)量

maxWorkerThreads: CLR中真實處理請求的***Worker線程數(shù)量

minIoThreads: 從HTTP.SYS隊列中拿請求的最小IO線程數(shù)量

minWorkerThreads:CLR中真實處理請求的最小Worker線程數(shù)量

minIoThreads和minWorkerThreads的默認(rèn)值是1,合理的加大他們可以避免不必要的線程創(chuàng)建和銷毀工作。maxIoThreads如果設(shè)置太大的話,或者說不合理的話就會導(dǎo)致多數(shù)的request被放到進(jìn)程池級別的隊列中。所以 maxIoThreads和maxWorkerThreads是有一定關(guān)系的,假設(shè)1個worker線程1s可以處理10個請求,如果我們的機(jī)器配置只允許我們同時處理100個請求,那我們合理的maxThreads就是10。但是IO線程并不需要10s去處理一個請求,它比woker線程快,因為它只要從HTTP.SYS的隊列那里拿過來就可以了,我們假設(shè)一個IO線程1s可以處理20個請求,對應(yīng)100個請求的上限,那我們maxIoThreads的合理值應(yīng)該是5。

***并行處理請求數(shù)量

進(jìn)程池級別的隊列給我們的CLR一定的緩沖,這里面要注意的是,這個隊列還沒有進(jìn)入到CLR,所以它不會占用我們托管環(huán)境的任何資源,也就是把請求卡在了CLR的外面。我們需要在aspnet.config級別進(jìn)行配置,我們可以在.net fraemwork的安裝目錄下找到它。一般是 C:\Windows\Microsoft.NET\Framework\v4.0.30319 如果你安裝的是4.0的話。

如何解析異步編程In .NET APM/EAP和async/await

maxConcurrentRequestPerCPU: 每個CPU所允許的***并行處理請求數(shù)量,當(dāng)CLR中worker線程正在處理的請求之和大于這個數(shù)時,從IO線程過來的請求就會被放到我們進(jìn)程池級別的隊列中。

maxConcurrentThreadsPerCPU: 設(shè)置為0即禁用。

requestQueueLimit:這個就是我們HTTP.SYS中隊列的那個長度,我們可以在web.config/system.web/processModel結(jié)點下配置。

async和await 做了什么?

我們終于要切入正題了,拿ASP.NET MVC舉例,如果不采用async的Action,那么毫無疑問,它是在一個Woker線程中執(zhí)行的。當(dāng)我們訪問一些web service,或者讀文件的時候,這個Worker線程就會被阻塞。假設(shè)我們這個Action執(zhí)行時間一共是100ms,其它訪問web service花了80ms,理想情況下一個Worker線程一秒可以響應(yīng)10個請求,假設(shè)我們的maxWorkerThreads是10,那我們一秒內(nèi)總是可響應(yīng)請求就是100。如果說我們想把這個可響應(yīng)請求數(shù)升到200怎么做呢?

有人會說,這還不簡單,把maxWorkerThreads調(diào)20不就行了么? 其實我們做也沒有什么 問題,確實是可以的,而且也確實能起到作用。那我們?yōu)槭裁催€要大費周章的搞什么 async/await呢?搞得腦子都暈了?async/await給我們解決了什么問題?它可以在我們訪問web service的時候把當(dāng)前的worker線程放走,將它放回線程池,這樣它就可以去處理其它的請求了。和IO線程一樣,IO線程只負(fù)責(zé)把請求交給Worker線程或者放入進(jìn)程池級別的隊列,然后又去HTTP.SYS的隊列中處理其它的請求。等到web service給我們返回結(jié)果了,會再到線程池中隨機(jī)拿一個新的woker線程繼續(xù)往下執(zhí)行。也就是說我們減少了那一部分等待的時間,充份利用了線程。

我們來對比一下使用async/awit和不使用的情況,

不使用async/await: 20個woker線程1s可以處理200個請求。

那轉(zhuǎn)換成總的時間的就是 20 * 1000ms =  20000ms,

其中等待的時間為 200 * 80ms = 16000ms。

也就是說使用async/await我們至少節(jié)約了16000ms的時間,這20個worker線程又會再去處理請求,即使按照每個請求100ms的處理時間我們還可以再增加160個請求。而且別忘了100ms是基于同步情況下,包括等待時間在內(nèi)的基礎(chǔ)上得到的,所以實際情況可能還要多,當(dāng)然我們這里沒有算上線程切換的時間,所以實際情況中是有一點差異的,但是應(yīng)該不會很大,因為我們的線程都是基于線程池的操作。

所有結(jié)果是20個Worker線程不使用異步的情況下,1s能自理200個請求,而使用異步的情況下可以處理360個請求,立馬提升80%呀!采用異步之后,對于同樣的請求數(shù)量,需要的Worker線程數(shù)據(jù)會大大減少50%左右,一個線程至少會在堆上分配1M的內(nèi)存,如果是1000個線程那就是1G的容量,雖然內(nèi)存現(xiàn)在便宜,但是省著總結(jié)是好的嘛,而且更少的線程是可以減少線程池在維護(hù)線程時產(chǎn)生的CPU消耗的。

注意:以上數(shù)據(jù)并非真實測試數(shù)據(jù),真實情況一個request的時間也并非100ms,花費在web service上的時間也并非80ms,僅僅是給大家一個思路:),所以這里面用了async和await之后對響應(yīng)能力有多大的提升和我們原來堵塞在這些IO和網(wǎng)絡(luò)上的時間是有很大的關(guān)系的。

幾點建議

看到這里,不知道大家有沒有得到點什么。首先***點我們要知道的是async/await不是***藥,不們不能指望光寫兩個光鍵字就希望性能的提升。要記住,一個CPU在同一時間段內(nèi)是只能執(zhí)行一個線程的。所以這也是為什么async和await建議在IO或者是網(wǎng)絡(luò)操作的時候使用。我們的MVC站點訪問WCF或者Web Service這種場景就非常的適合使用異步來操作。在上面的例子中80ms讀取web service的時間,大部份時間都是不需要cpu操作的,這樣cpu才可以被其它的線程利用,如果不是一個讀取web service的操作,而是一個復(fù)雜計算的操作,那你就等著cpu爆表吧。

第二點是,除了程序中利用異步,我們上面講到的關(guān)于IIS的配置是很重要的,如果使用了異步,請記得把maxWorkerThreads和maxConcurrentRequestPerCPU的值調(diào)高試試。

早期對Web service的異步編程模式APM

講完我們高大上的async/await之后,我們來看看這個技術(shù)很老,但是概念確依舊延續(xù)至今的Web Service。 我們這里所說的針對web service的異步編程模式不是指在服務(wù)器端的web service本身,而是指調(diào)用web service的客戶端。大家知道對于web service,我們通過添加web service引用或者.net提供的生成工具就可以生成相應(yīng)的代理類,可以讓我們像調(diào)用本地代碼一樣訪問web service,而所生成的代碼類中對針對每一個web service方法生成3個對應(yīng)的方法,比如說我們的方法名叫DownloadContent,除了這個方法之外還有BeginDownloadContent和EndDownloadContent方法,而這兩個就是我們今天要說的早期的異步編程模式APM(Asynchronous Programming Model)。下面就來看看我們web service中的代碼,注意我們現(xiàn)在的項目都是在.NET Framework3.5下實現(xiàn)的。

PageContent.asmx的代碼

public class PageContent : System.Web.Services.WebService  {      [WebMethod]      public string DownloadContent(string url)      {          var client = new System.Net.WebClient();          return client.DownloadString(url);      }  }

注意我們web service中的DownloadContent方法調(diào)用的是WebClient的同步方法,WebClient也有異步方法即:DownloadStringAsync。但是大家要明白,不管服務(wù)器是同步還是異步,對于客戶端來說調(diào)用了你這個web service都是一樣的,就是得等你返回結(jié)果。

當(dāng)然,我們也可以像MVC里面的代碼一樣,把我們的服務(wù)器端也寫成異步的。那得到好處的是那個托管web service的服務(wù)器,它的處理能力得到提高,就像ASP.NET一樣。如果我們用JavaScript去調(diào)用這個Web Service,那么Ajax(Asynchronous Javascript + XML)就是我們客戶端用到的異步編程技術(shù)。如果是其它的客戶端呢?比如說一個CS的桌面程序?我們需要異步編程么?

當(dāng)WinForm遇上Web Service

WinForm不像托管在IIS的ASP.NET網(wǎng)站,會有一個線程池管理著多個線程來處理用戶的請求,換個說法ASP.NET網(wǎng)站生來就是基于多線程的。但是,在WinForm中,如果我們不刻意使用多線程,那至始至終,都只有一個線程,稱之為UI線程。也許在一些小型的系統(tǒng)中WinForm很少涉及到多線程,因為WinForm本身的優(yōu)勢就在它是獨立運行在客戶端的,在性能上和可操作性上都會有很大的優(yōu)勢。所以很多中小型的WinForm系統(tǒng)都是直接就訪問數(shù)據(jù)庫了,并且基本上也只有數(shù)據(jù)的傳輸,什么圖片資源那是很少的,所以等待的時間是很短的,基本不用費什么腦力去考慮什么3秒之內(nèi)必須將頁面顯示到用戶面前這種問題。

既然WinForm在性能上有這么大的優(yōu)勢,那它還需要異步嗎?

我們上面說的是中小型的WinForm,如果是大型的系統(tǒng)呢?如果WinForm只是其它的很小一部分,就像我們文章開始說的還有很多其它成千上萬個手機(jī)客戶端,Web客戶端,平板客戶端呢?如果客戶端很多導(dǎo)致數(shù)據(jù)庫撐不住怎么辦? 想在中間加一層緩存怎么辦?

拿一個b2b的網(wǎng)站功能舉例,用戶可以通過網(wǎng)站下單,手機(jī)也可以下單,還可以通過電腦的桌面客戶端下單。在下完單之后要完成交易,庫存扣減,發(fā)送訂單確認(rèn)通知等等功能,而不管你的訂單是通過哪個端完成的,這些功能我們都要去做,對嗎?那我們就不能單獨放在WinForm里面了,不然這些代碼在其它的端里面又得全部全新再一一實現(xiàn),同樣的代碼放在不同的地方那可是相當(dāng)危險的,所以就有了我們后來的SOA架構(gòu),把這些功能都抽成服務(wù),每種類型的端都是調(diào)用服務(wù)就可以了。一是可以統(tǒng)一維護(hù)這些功能,二是可以很方便的做擴(kuò)展,去更好的適應(yīng)功能和架構(gòu)上的擴(kuò)展。比如說像下面這樣的一個系統(tǒng)。

如何解析異步編程In .NET APM/EAP和async/await

在上圖中,Web端雖然也是屬于我們平常說的服務(wù)端(甚至是由多臺服務(wù)器組成的web群集),但是對我們整個系統(tǒng)來說,它也只是一個端而已。對于一個端來說,它本身只處理和用戶交互的問題,其余所有的功能,業(yè)務(wù)都會交給后來臺處理。在我們上面的架構(gòu)中,應(yīng)用層都不會直接參加真正業(yè)務(wù)邏輯相關(guān)的處理,而是放到我們更下層數(shù)據(jù)層去做處理。那么應(yīng)用層主要協(xié)助做一些與用戶交互的一些功能,如果手機(jī)短信發(fā)送,郵件發(fā)送等等,并且可以根據(jù)優(yōu)先級選擇是放入隊列中稍候處理還是直接調(diào)用功能服務(wù)立即處理。

在這樣的一個系統(tǒng)中,我們的Web服務(wù)器也好,Winform端也好都將只是整個系統(tǒng)中的一個終端,它們主要的任何是用戶和后面服務(wù)之間的一個橋梁。涉及到Service的調(diào)用之后,為了給用戶良好的用戶體驗,在WinForm端,我們自然就要考慮異步的問題。

WinForm異步調(diào)用Web Service

有了像VS這樣強(qiáng)大的工具為我們生成代理類,我們在寫調(diào)用Web service的代碼時就可以像調(diào)用本地類庫一樣調(diào)用Web Service了,我們只需要添加一個Web Reference就可以了。

如何解析異步編程In .NET APM/EAP和async/await

// Form1.cs的代碼

private void button1_Click(object sender, EventArgs e)  {      var pageContentService = new localhost.PageContent();      pageContentService.BeginDownloadContent(          "http://jesse2013.cnblogs.com",          new AsyncCallback(DownloadContentCallback),          pageContentService);  }     private void DownloadContentCallback(IAsyncResult result)  {      var pageContentService = (localhost.PageContent)result.AsyncState;      var msg = pageContentService.EndDownloadContent(result);      MessageBox.Show(msg);  }

代碼非常的簡單,在執(zhí)行完pageContentService.BeginDownloadContent之后,我們的主線程就返回了。在調(diào)用Web service這段時間內(nèi)我們的UI不會被阻塞,也不會出現(xiàn)“無法響應(yīng)這種情況”,我們依然可以拖動窗體甚至做其它的事情。這就是APM的魔力,但是我們的callback究竟是在哪個線程中執(zhí)行的呢?是線程池中的線程么?咋們接著往下看。

APM異步編程模式詳解

線程問題

接下來我們就是更進(jìn)一步的了解APM這種模式是如何工作的,但是首先我們要回答上面留下來的問題,這種異步的編程方式有沒有為我們開啟新的線程?讓代碼說話:

private void button1_Click(object sender, EventArgs e)  {      Trace.TraceInformation("Is current thread from thread pool? {0}", Thread.CurrentThread.IsThreadPoolThread ? "Yes" : "No");      Trace.TraceInformation("Start calling web service on thread: {0}", Thread.CurrentThread.ManagedThreadId);      var pageContentService = new localhost.PageContent();      pageContentService.BeginDownloadContent(          "http://jesse2013.cnblogs.com",          new AsyncCallback(DownloadContentCallback),          pageContentService);  }     private void DownloadContentCallback(IAsyncResult result)  {      var pageContentService = (localhost.PageContent)result.AsyncState;      var msg = pageContentService.EndDownloadContent(result);         Trace.TraceInformation("Is current thread from thread pool? {0}" , Thread.CurrentThread.IsThreadPoolThread ? "Yes" : "No");      Trace.TraceInformation("End calling web service on thread: {0}, the result of the web service is: {1}",          Thread.CurrentThread.ManagedThreadId,          msg);  }

我們在按鈕點擊的方法和callback方法中分別輸出當(dāng)前線程的ID,以及他們是否屬于線程池的線程,得到的結(jié)果如下:

Desktop4.0.vshost.exe Information: 0 : Is current thread a background thread? NO  Desktop4.0.vshost.exe Information: 0 : Is current thread from thread pool? NO  Desktop4.0.vshost.exe Information: 0 : Start calling web service on thread: 9  Desktop4.0.vshost.exe Information: 0 : Is current thread a background thread? YES  Desktop4.0.vshost.exe Information: 0 : Is current thread from thread pool? YES  Desktop4.0.vshost.exe Information: 0 : End calling web service on thread: 14, the result of the web service is: <!DOCTYPE html>...

按鈕點擊的方法是由UI直接控制,很明顯它不是一個線程池線程,也不是后臺線程。而我們的callback卻是在一個來自于線程池的后臺線程執(zhí)行的,答案揭曉了,可是這會給我們帶來一個問題,我們上面講了只有UI線程也可以去更新我們的UI控件,也就是說在callback中我們是不能更新UI控件的,那我們?nèi)绾巫尭耈I讓用戶知道反饋呢?答案在后面接曉 :),讓我們先專注于把APM弄清楚。

從Delegate開始

其實,APM在.NET3.5以前都被廣泛使用,在WinForm窗體控制中,在一個IO操作的類庫中等等!大家可以很容易的找到搭配了Begin和End的方法,更重要的是只要是有代理的地方,我們都可以使用APM這種模式。我們來看一個很簡單的例子:

delegate void EatAsync(string food);  private void button2_Click(object sender, EventArgs e)  {      var myAsync = new EatAsync(eat);      Trace.TraceInformation("Activate eating on thread: {0}", Thread.CurrentThread.ManagedThreadId);      myAsync.BeginInvoke("icecream", new AsyncCallback(clean), myAsync);  }     private void eat(string food)  {      Trace.TraceInformation("I am eating.... on thread: {0}", Thread.CurrentThread.ManagedThreadId);  }     private void clean(IAsyncResult asyncResult)  {      Trace.TraceInformation("I am done eating.... on thread: {0}", Thread.CurrentThread.ManagedThreadId);  }

上面的代碼中,我們通過把eat封裝成一個委托,然后再調(diào)用該委托的BeginInvoke方法實現(xiàn)了異步的執(zhí)行。也就是實際的eat方法不是在主線程中執(zhí)行的,我們可以看輸出的結(jié)果:

Desktop4.0.vshost.exe Information: 0 : Activate eating on thread: 10  Desktop4.0.vshost.exe Information: 0 : I am eating.... on thread: 6  Desktop4.0.vshost.exe Information: 0 : I am done eating.... on thread: 6

clean是我們傳進(jìn)去的callback,該方法會在我們的eat方法執(zhí)行完之后被調(diào)用,所以它會和我們eat方法在同一個線程中被調(diào)用。大家如果熟悉代理的話就會知道,代碼實際上會被編譯成一個類,而BeginInvoke和EndInvoke方法正是編譯器為我們自動加進(jìn)去的方法,我們不用額外做任何事情,這在早期沒有TPL和async/await之前(APM從.NET1.0時代就有了),的確是一個不錯的選擇。

再次認(rèn)識APM

了解了Delegate實現(xiàn)的BeginInvoke和EndInvoke之后,我們再來分析一下APM用到的那些對象。 拿我們Web service的代理類來舉例,它為我們生成了以下3個方法:

  1. string DownloadContent(string url): 同步方法

  2. IAsyncResult BeginDownloadContent(string url, AsyncCallback callback, object asyncState): 異步開始方法

  3. EndDownloadContent(IAsyncResult asyncResult):異步結(jié)束方法

在我們調(diào)用EndDownloadContent方法的時候,如果我們的web service調(diào)用還沒有返回,那這個時候就會用阻塞的方式去拿結(jié)果。但是在我們傳到BeginDownloadContent中的callback被調(diào)用的時候,那操作一定是已經(jīng)完成了,也就是說IAsyncResult.IsCompleted = true。而在APM異步編程模式中Begin方法總是返回IAsyncResult這個接口的實現(xiàn)。IAsyncReuslt僅僅包含以下4個屬性:

如何解析異步編程In .NET APM/EAP和async/await

WaitHanlde通常作為同步對象的基類,并且可以利用它來阻塞線程,更多信息可以參考MSDN 。 借助于IAsyncResult的幫助,我們就可以通過以下幾種方式去獲取當(dāng)同所執(zhí)行操作的結(jié)果。

  1. 輪詢

  2. 強(qiáng)制等待

  3. 完成通知

完成通知就是們上面用到的那種,調(diào)完Begin方法之后,主線程就算完成任務(wù)了。我們也不用監(jiān)控該操作的執(zhí)行情況,當(dāng)該操作執(zhí)行完之后,我們在Begin方法中傳進(jìn)去的callback就會被調(diào)用了,我們可以在那個方法中調(diào)用End方法去獲取結(jié)果。下面我們再簡單說一下前面兩種方式。

//輪詢獲取結(jié)果代碼

var pageContentService = new localhost.PageContent();  IAsyncResult asyncResult = pageContentService.BeginDownloadContent(      "http://jesse2013.cnblogs.com",      null,      pageContentService);     while (!asyncResult.IsCompleted)  {      Thread.Sleep(100);  }  var content = pageContentService.EndDownloadContent(asyncResult);

// 強(qiáng)制等待結(jié)果代碼

var pageContentService = new localhost.PageContent();  IAsyncResult asyncResult = pageContentService.BeginDownloadContent(      "http://jesse2013.cnblogs.com",      null,      pageContentService);     // 也可以調(diào)用WaitOne()的無參版本,不限制強(qiáng)制等待時間  if (asyncResult.AsyncWaitHandle.WaitOne(2000))  {      var content = pageContentService.EndDownloadContent(asyncResult);  }  else {       // 2s時間已經(jīng)過了,但是還沒有執(zhí)行完     }

EAP(Event-Based Asynchronous Pattern)

EAP是在.NET2.0推出的另一種過渡的異步編程模型,也是在.NET3.5以后Microsoft支持的一種做法,為什么呢? 如果大家建一個.NET4.0或者更高版本的WinForm項目,再去添加Web Reference就會發(fā)現(xiàn)生成的代理類中已經(jīng)沒有Begin和End方法了,記住在3.5的時候是兩者共存的,你可以選擇任意一種來使用。但是到了.NET4.0以后,EAP成為了你唯一的選擇。(我沒有嘗試過手動生成代理類,有興趣的同學(xué)可以嘗試一下)讓我們來看一下在.NET4下,我們是如何異步調(diào)用Web Service的。

private void button1_Click(object sender, EventArgs e)  {      var pageContent = new localhost.PageContent();      pageContent.DownloadContentAsync("http://jesse2013.cnblogs.com");      pageContent.DownloadContentCompleted += pageContent_DownloadContentCompleted;  }     private void pageContent_DownloadContentCompleted(object sender, localhost.DownloadContentCompletedEventArgs e)  {      if (e.Error == null)      {          textBox1.Text = e.Result;      }      else     {           // 出錯了      }  }

線程問題

不知道大家還是否記得,在APM模式中,callback是執(zhí)行在另一個線程中,不能隨易的去更新UI。但是如果你仔細(xì)看一下上面的代碼,我們的DownloadContentCompleted事件綁定的方法中直接就更新了UI,把返回的內(nèi)容寫到了一個文本框里面。通過同樣的方法可以發(fā)現(xiàn),在EAP這種異步編程模式下,事件綁定的方法也是在調(diào)用的那個線程中執(zhí)行的。也就是說解決了異步編程的時候UI交互的問題,而且是在同一個線程中執(zhí)行。 看看下面的代碼:

private void button1_Click(object sender, EventArgs e)  {      Trace.TraceInformation("Call DownloadContentAsync on thread: {0}", Thread.CurrentThread.ManagedThreadId);      Trace.TraceInformation("Is current from thread pool? : {0}", Thread.CurrentThread.IsThreadPoolThread ? "YES" : "NO");         var pageContent = new localhost.PageContent();      pageContent.DownloadContentAsync("http://jesse2013.cnblogs.com");      pageContent.DownloadContentCompleted += pageContent_DownloadContentCompleted;  }     private void pageContent_DownloadContentCompleted(object sender, localhost.DownloadContentCompletedEventArgs e)  {      Trace.TraceInformation("Completed DownloadContentAsync on thread: {0}", Thread.CurrentThread.ManagedThreadId);      Trace.TraceInformation("Is current from thread pool? : {0}", Thread.CurrentThread.IsThreadPoolThread ? "YES" : "NO");  }
Desktop4.vshost.exe Information: 0 : Call DownloadContentAsync on thread: 10  Desktop4.vshost.exe Information: 0 : Is current from thread pool? : NO  Desktop4.vshost.exe Information: 0 : Completed DownloadContentAsync on thread: 10  Desktop4.vshost.exe Information: 0 : Is current from thread pool? : NO

async/await 給WinFrom帶來了什么

如果說async給ASP.NET帶來的是處理能力的提高,那么在WinForm中給程序員帶來的好處則是***的。我們再也不用因為要實現(xiàn)異步寫回調(diào)或者綁定事件了,省事了,可讀性也提高了。不信你看下面我們將調(diào)用我們那個web service的代碼在.NET4.5下實現(xiàn)一下:

private async void button2_Click(object sender, EventArgs e)  {      var pageContent = new localhost.PageContentSoapClient();      var content = await pageContent.DownloadContentAsync("http://jesse2013.cnblogs.com");         textBox1.Text = content.Body.DownloadContentResult;  }

簡單的三行代碼,像寫同步代碼一樣寫異步代碼,我想也許這就是async/await的魔力吧。在await之后,UI線程就可以回去響應(yīng)UI了,在上面的代碼中我們是沒有新線程產(chǎn)生的,和EAP一樣拿到結(jié)果直接就可以對UI操作了。

async/await似乎真的很好,但是如果我們await后面的代碼執(zhí)行在另外一個線程中會發(fā)生什么事情呢?

private async void button1_Click(object sender, EventArgs e)  {      label1.Text = "Calculating Sqrt of 5000000";      button1.Enabled = false;      progressBar1.Visible = true;         double sqrt = await Task<double>.Run(() =>      {          double result = 0;          for (int i = 0; i < 50000000; i++)          {              result += Math.Sqrt(i);                 progressBar1.Maximum = 50000000;              progressBar1.Value = i;          }          return result;      });         progressBar1.Visible = false;      button1.Enabled = true;      label1.Text = "The sqrt of 50000000 is " + sqrt;  }

我們在界面中放了一個ProgressBar,同時開一個線程去把從1到5000000的平方全部加起來,看起來是一個非常耗時的操作,于是我們用Task.Run開了一個新的線程去執(zhí)行。(注:如果是純運算的操作,多線程操作對性能沒有多大幫助,我們這里主要是想給UI一個進(jìn)度顯示當(dāng)前進(jìn)行到哪一步了。)看起來沒有什么問題,我們按F5運行吧!
Bomb~

如何解析異步編程In .NET APM/EAP和async/await

當(dāng)執(zhí)行到這里的時候,程序就崩潰了,告訴我們”無效操作,只能從創(chuàng)建porgressBar的線程訪問它?!? 這也是我們一開始提到的,在WinForm程序中,只有UI主線程才能對UI進(jìn)行操作,其它的線程是沒有權(quán)限的。接下來我們就來看看,如果在WinForm中實現(xiàn)非UI線程對UI控制的更新操作。

不同線程之間通訊的問題

***的Invoke

WinForm中絕大多數(shù)的控件包括窗體在內(nèi)都實現(xiàn)了Invoke方法,可以傳入一個Delegate,這個Delegate將會被擁有那個控制的線程所調(diào)用,從而避免了跨線程訪問的問題。

Trace.TraceInformation("UI Thread : {0}", Thread.CurrentThread.ManagedThreadId);  double sqrt = await Task<double>.Run(() =>  {      Trace.TraceInformation("Run calculation on thread: {0}", Thread.CurrentThread.ManagedThreadId);      double result = 0;      for (int i = 0; i < 50000000; i++)      {          result += Math.Sqrt(i);          progressBar1.Invoke(new Action(() => {              Trace.TraceInformation("Update UI on thread: {0}", Thread.CurrentThread.ManagedThreadId);              progressBar1.Maximum = 50000000;              progressBar1.Value = i;          }));      }      return result;  });
Desktop.vshost.exe Information: 0 : UI Thread : 9  Desktop.vshost.exe Information: 0 : Run calculation on thread: 10  Desktop.vshost.exe Information: 0 : Update UI on thread: 9

Invoke方法比較簡單,我們就不做過多的研究了,但是我們要考慮到一點,Invoke是WinForm實現(xiàn)的UI同交互技術(shù),WPF用的卻是Dispatcher,如果是在ASP.NET下跨線程之間的同步又怎么辦呢。為了兼容各種技術(shù)平臺下,跨線程同步的問題,Microsoft在.NET2.0的時候就引入了我們下面的這個對象。

SynchronizationContext上下文同步對象

為什么需要SynchronizationContext

就像我們在WinForm中遇到的問題一樣,有時候我們需要在一個線程中傳遞一些數(shù)據(jù)或者做一些操作到另一個線程。但是在絕大多數(shù)情況下這是不允許的,出于安全因素的考慮,每一個線程都有它獨立的內(nèi)存空間和上下文。因此在.NET2.0,微軟推出了SynchronizationContext。

它主要的功能之一是為我們提供了一種將一些工作任務(wù)(Delegate)以隊列的方式存儲在一個上下文對象中,然后把這些上下文對象關(guān)聯(lián)到具體的線程上,當(dāng)然有時候多個線程也可以關(guān)聯(lián)到同一個SynchronizationContext對象。獲取當(dāng)前線程的同步上下文對象可以使用SynchronizationContext.Current。同時它還為我們提供以下兩個方法Post和Send,分別是以異步和同步的方法將我們上面說的工作任務(wù)放到我們SynchronizationContext的隊列中。

SynchronizationContext示例

還是拿我們上面Invoke中用到的例子舉例,只是這次我們不直接調(diào)用控件的Invoke方法去更新它,而是寫了一個Report的方法專門去更新UI。

double sqrt = await Task<double>.Run(() =>  {      Trace.TraceInformation("Current thread id is:{0}", Thread.CurrentThread.ManagedThreadId);         double result = 0;      for (int i = 0; i < 50000000; i++)      {          result += Math.Sqrt(i);          Report(new Tuple<int, int>(50000000, i));      }      return result;  });

每一次操作完之后我們調(diào)用一下Report方法,把我們總共要算的數(shù)字,以前當(dāng)前正在計算的數(shù)字傳給它就可以了。接下來就看我們的Report方法了。

private SynchronizationContext m_SynchronizationContext;  private DateTime m_PreviousTime = DateTime.Now;     public Form1()  {      InitializeComponent();      // 在全局保存當(dāng)前UI線程的SynchronizationContext對象      m_SynchronizationContext = SynchronizationContext.Current;  }     public void Report(Tuple<int, int> value)  {      DateTime now = DateTime.Now;      if ((now - m_PreviousTime).Milliseconds > 100)      {          m_SynchronizationContext.Post((obj) =>          {              Tuple<int, int> minMax = (Tuple<int, int>)obj;              progressBar1.Maximum = minMax.Item1;              progressBar1.Value = minMax.Item2;          }, value);             m_PreviousTime = now;      }  }

整個操作看起來要比Inovke復(fù)雜一點,與Invoke不同的是SynchronizationContext不需要對Control的引用,而Invoke必須先得有那個控件才能調(diào)用它的Invoke方法去更新它。

小結(jié)

這篇博客內(nèi)容有點長,不知道有多少人可以看到這里:)。最開始我只是想寫寫WinFrom下異步調(diào)用Web Service的一些東西,在我開始這篇文件的題目是”異步編程在WinForm下的實踐“,但是寫著寫著發(fā)現(xiàn)越來越多的迷團(tuán)沒有解開,其實都是一些老的技術(shù)以前沒有接觸和掌握好,所以所幸就一次性把他們都重新學(xué)習(xí)了一遍,與大家分享。

我們再來回顧一下文章所涉及到的一些重要的概念:

  1. async/await 在ASP.NET做的***貢獻(xiàn)(早期ASP.NET的異步開發(fā)模式同樣也有這樣的貢獻(xiàn)),是在訪問數(shù)據(jù)庫的時候、訪問遠(yuǎn)程IO的時候及時釋放了當(dāng)前的處理性程,可以讓這些線程回到線程池中,從而實現(xiàn)可以去處理其它請求的功能。

  2. 異步的ASP.NET開發(fā)能夠在處理能力上帶來多大的提高,取決于我們的程序有多少時間是被阻塞的,也就是那些訪問數(shù)據(jù)庫和遠(yuǎn)程Service的時間。

  3. 除了將代碼改成異步,我們還需要在IIS上做一些相對的配置來實現(xiàn)***化。

  4. 不管是ASP.NET、WinForm還是Mobile、還是平板,在大型系統(tǒng)中都只是一個與用戶交互的端而已,所以不管你現(xiàn)在是做所謂的前端(JavaScript + CSS等),還是所謂的后端(ASP.NET MVC、WCF、Web API 等 ),又或者是比較時髦的移動端(IOS也好,Andrioid也罷,哪怕是不爭氣的WP),都只是整個大型系統(tǒng)中的零星一角而已。當(dāng)然我并不是貶低這些端的價值,正是因為我們專注于不同,努力提高每一個端的用戶體驗,才能讓這些大型系統(tǒng)有露臉的機(jī)會。我想說的是,在你對現(xiàn)在技術(shù)取得一定的成就之后,不要停止學(xué)習(xí),因為整個軟件架構(gòu)體系中還有很多很多美妙的東西值得我們?nèi)グl(fā)現(xiàn)。

  5. APM和EAP是在async/await之前的兩種不同的異步編程模式。

  6. APM如果不阻塞主線程,那么完成通知(回調(diào))就會執(zhí)行在另外一個線程中,從而給我們更新UI帶來一定的問題。

  7. EAP的通知事件是在主線程中執(zhí)行的,不會存在UI交互的問題。

  8. ***,我們還學(xué)習(xí)了在Winform下不同線程之間交互的問題,以及SynchronizationContext。

  9. APM是.NET下最早的異步編程方法,從.NET1.0以來就有了。在.NET2.0的時候,微軟意識到了APM的回調(diào)函數(shù)中與UI交互的問題,于是帶來了新的EAP。APM與EAP一直共存到.NET3.5,在.NET4.0的時候微軟帶來了TPL,也就是我們所熟知的Task編程,而.NET4.5就是我們大家知道的async/await了,可以看到.NET一直在不停的進(jìn)步,加上最近不斷的和開源社區(qū)的合作,跨平臺等特性的引入,我們有理由相信.NET會越走越好。

看完上述內(nèi)容,你們掌握如何解析異步編程In .NET APM/EAP和async/await的方法了嗎?如果還想學(xué)到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI