如何解析異步編程In .NET APM/EAP和async/await,相信很多沒有經(jīng)驗(yàn)的人對(duì)此束手無策,為此本文總結(jié)了問題出現(xiàn)的原因和解決方法,通過這篇文章希望你能解決這個(gè)問題。
專注于為中小企業(yè)提供成都網(wǎng)站建設(shè)、成都網(wǎng)站設(shè)計(jì)服務(wù),電腦端+手機(jī)端+微信端的三站合一,更高效的管理,為中小企業(yè)廣豐免費(fèi)做網(wǎng)站提供優(yōu)質(zhì)的服務(wù)。我們立足成都,凝聚了一批互聯(lián)網(wǎng)行業(yè)人才,有力地推動(dòng)了成百上千家企業(yè)的穩(wěn)健成長(zhǎng),幫助中小企業(yè)通過網(wǎng)站建設(shè)實(shí)現(xiàn)規(guī)模擴(kuò)充和轉(zhuǎn)變。
在之前寫的一篇關(guān)于async和await的前世今生的文章之后,大家似乎在async和await提高網(wǎng)站處理能力方面還有一些疑問,博客園本身也做了不少的嘗試。今天我們?cè)賮砘卮鹨幌逻@個(gè)問題,同時(shí)我們會(huì)做一個(gè)async和await在WinForm中的嘗試,并且對(duì)比在4.5之前的異步編程模式APM/EAP和async/await的區(qū)別,***我們還會(huì)探討在不同線程之間交互的問題。
IIS存在著處理能力的問題,但是WinForm卻是UI響應(yīng)的問題,并且WinForm的UI線程至始至終都是同一個(gè),所以兩者之間有一定的區(qū)別。有人會(huì)問,現(xiàn)在還有人寫WinForm嗎?好吧,它確是一個(gè)比較老的東西呢,不如WPF炫,技術(shù)也不如WPF先進(jìn),但是從架構(gòu)層面來講,不管是Web,還是WinForm,又或是WPF,Mobile,這些都只是表現(xiàn)層,不是么?現(xiàn)在的大型系統(tǒng)一般桌面客戶端,Web端,手機(jī),平板端都會(huì)涉及,這也是為什么會(huì)有應(yīng)用層,服務(wù)層的存在。我們?cè)谶@談?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)的問題,我們后面再慢慢深入,另外別說我沒有提示您,我們今天還會(huì)看到.NET中另一個(gè)已經(jīng)老去的技術(shù)Web Service。
還得提示您,文章內(nèi)容有點(diǎn)長(zhǎng),涉及的知識(shí)點(diǎn)比較多,所以,我推薦:”先頂后看“ ,先頂后看是21世紀(jì)看長(zhǎng)篇的***之道,是良好溝通的開端,想知道是什么會(huì)讓你與眾不同嗎?想知道為什么上海今天會(huì)下這么大的雨么?請(qǐng)記住先頂后看,你頂?shù)牟皇俏业奈恼拢俏覀兠爸笥赀€要去上班的可貴精神!先頂后看,你值得擁有!
首先響應(yīng)能力并不完全是說我們程序性能的問題,有時(shí)候可能你的程序沒有任何問題,而且精心經(jīng)過優(yōu)化,可是響應(yīng)能力還是沒有上去,網(wǎng)站性能分析是一個(gè)復(fù)雜的活,有時(shí)候只能靠經(jīng)驗(yàn)和不斷的嘗試才能達(dá)到比較好的效果。當(dāng)然我們今天討論的主要是IIS的處理能力,或者也可能說是IIS的性能,但絕非代碼本身的性能。即使async/await能夠提高IIS的處理能力,但是對(duì)于用戶來說整個(gè)頁面從發(fā)起請(qǐng)求到頁面渲染完成的這些時(shí)間,是不會(huì)因?yàn)槲覀兗恿薬sync/await之后產(chǎn)生多大變化的。
另外異步的ASP.NET并非只有async/await才可以做的,ASP.NET在Web Form時(shí)代就已經(jīng)有異步Page了,包括ASP.NET MVC不是也有異步的Controller么?async/await 很新,很酷,但是它也只是在原有一技術(shù)基礎(chǔ)上做了一些改進(jìn),讓程序員們寫起異步代碼來更容易了。大家常說微軟喜歡新瓶裝舊酒,至少我們要看到這個(gè)新瓶給我們帶來了什么,不管是任何產(chǎn)品,都不可能一開始就很***,所以不斷的迭代更新,也可以說是一種正確做事的方式。
ASP.NET是如何在IIS中工作的一文已經(jīng)很詳細(xì)的介紹了一個(gè)請(qǐng)求是如何從客戶端到服務(wù)器的HTTP.SYS***進(jìn)入CLR進(jìn)行處理的(強(qiáng)烈建議不了解這一塊的同學(xué)先看這篇文章,有助于你理解本小節(jié)),但是所有的步驟都是基于一個(gè)線程的假設(shè)下進(jìn)行的。IIS本身就是一個(gè)多線程的工作環(huán)境,如果我們從多線程的視角來看會(huì)發(fā)生什么變化呢?我們首先來看一下下面這張圖。注意:我們下面的步驟是建立在IIS7.0以后的集成模式基礎(chǔ)之上的。
我們?cè)賮硎崂硪幌律厦娴牟襟E:
所有的請(qǐng)求最開始是由HTTP.SYS接收的,HTTP.SYS內(nèi)部有一個(gè)隊(duì)列維護(hù)著這些請(qǐng)求,這個(gè)隊(duì)列的request的數(shù)量大于一定數(shù)量(默認(rèn)是1000)的時(shí)候,HTTP.SYS就會(huì)直接返回503狀態(tài)(服務(wù)器忙),這是我們的***個(gè)閥門。
HTTP.SYS把請(qǐng)求交給CLR 線程池中的IO線程
CLR 線程池中的 Worker線程從IO線程中接過請(qǐng)求來處理,IO線程不必等待該請(qǐng)求的處理結(jié)果,而是可以返回繼續(xù)去處理HTTP.SYS隊(duì)列中的請(qǐng)求。IO線程和Worker線程的數(shù)量上限是第二個(gè)閥門。
當(dāng)CLR中正在被處理的請(qǐng)求數(shù)據(jù)大于一定值(***并行處理請(qǐng)求數(shù)量)的時(shí)候,從IO線程過來的請(qǐng)求就不會(huì)直接交給Worker線程,而是放到一個(gè)進(jìn)程池級(jí)別的一個(gè)隊(duì)列了,等到這個(gè)數(shù)量小于臨界值的時(shí)候,才會(huì)把它再次交給Worker線程去處理。這是我們的第三個(gè)閥門。
從上面我們提到了幾大閥門中,我們可以得出下面的幾個(gè)數(shù)字控制或者說影響著我們的響應(yīng)能力。
HTTP.SYS隊(duì)列的長(zhǎng)度
***IO線程數(shù)量和***Worker線程數(shù)量
***并行處理請(qǐng)求數(shù)量
HTTP.SYS隊(duì)列的長(zhǎng)度
這個(gè)我覺得不需要額外解釋,默認(rèn)值是1000。這個(gè)值取決于我們我們后面CLR IO線程和Worker線程的處理速度,如果它們兩個(gè)都處理不了,這個(gè)數(shù)字再大也沒有用。因?yàn)?**他們會(huì)被存儲(chǔ)到進(jìn)程池級(jí)別的隊(duì)列中,所以只會(huì)造成內(nèi)存的浪費(fèi)。
***IO線程數(shù)量和***Worker線程數(shù)量
這兩個(gè)值是可以在web.config中進(jìn)行配置的。
maxIoThreads: 從HTTP.SYS隊(duì)列中拿請(qǐng)求的***IO線程數(shù)量
maxWorkerThreads: CLR中真實(shí)處理請(qǐng)求的***Worker線程數(shù)量
minIoThreads: 從HTTP.SYS隊(duì)列中拿請(qǐng)求的最小IO線程數(shù)量
minWorkerThreads:CLR中真實(shí)處理請(qǐng)求的最小Worker線程數(shù)量
minIoThreads和minWorkerThreads的默認(rèn)值是1,合理的加大他們可以避免不必要的線程創(chuàng)建和銷毀工作。maxIoThreads如果設(shè)置太大的話,或者說不合理的話就會(huì)導(dǎo)致多數(shù)的request被放到進(jìn)程池級(jí)別的隊(duì)列中。所以 maxIoThreads和maxWorkerThreads是有一定關(guān)系的,假設(shè)1個(gè)worker線程1s可以處理10個(gè)請(qǐng)求,如果我們的機(jī)器配置只允許我們同時(shí)處理100個(gè)請(qǐng)求,那我們合理的maxThreads就是10。但是IO線程并不需要10s去處理一個(gè)請(qǐng)求,它比woker線程快,因?yàn)樗灰獜腍TTP.SYS的隊(duì)列那里拿過來就可以了,我們假設(shè)一個(gè)IO線程1s可以處理20個(gè)請(qǐng)求,對(duì)應(yīng)100個(gè)請(qǐng)求的上限,那我們maxIoThreads的合理值應(yīng)該是5。
***并行處理請(qǐng)求數(shù)量
進(jìn)程池級(jí)別的隊(duì)列給我們的CLR一定的緩沖,這里面要注意的是,這個(gè)隊(duì)列還沒有進(jìn)入到CLR,所以它不會(huì)占用我們托管環(huán)境的任何資源,也就是把請(qǐng)求卡在了CLR的外面。我們需要在aspnet.config級(jí)別進(jìn)行配置,我們可以在.net fraemwork的安裝目錄下找到它。一般是 C:\Windows\Microsoft.NET\Framework\v4.0.30319 如果你安裝的是4.0的話。
maxConcurrentRequestPerCPU: 每個(gè)CPU所允許的***并行處理請(qǐng)求數(shù)量,當(dāng)CLR中worker線程正在處理的請(qǐng)求之和大于這個(gè)數(shù)時(shí),從IO線程過來的請(qǐng)求就會(huì)被放到我們進(jìn)程池級(jí)別的隊(duì)列中。
maxConcurrentThreadsPerCPU: 設(shè)置為0即禁用。
requestQueueLimit:這個(gè)就是我們HTTP.SYS中隊(duì)列的那個(gè)長(zhǎng)度,我們可以在web.config/system.web/processModel結(jié)點(diǎn)下配置。
我們終于要切入正題了,拿ASP.NET MVC舉例,如果不采用async的Action,那么毫無疑問,它是在一個(gè)Woker線程中執(zhí)行的。當(dāng)我們?cè)L問一些web service,或者讀文件的時(shí)候,這個(gè)Worker線程就會(huì)被阻塞。假設(shè)我們這個(gè)Action執(zhí)行時(shí)間一共是100ms,其它訪問web service花了80ms,理想情況下一個(gè)Worker線程一秒可以響應(yīng)10個(gè)請(qǐng)求,假設(shè)我們的maxWorkerThreads是10,那我們一秒內(nèi)總是可響應(yīng)請(qǐng)求就是100。如果說我們想把這個(gè)可響應(yīng)請(qǐng)求數(shù)升到200怎么做呢?
有人會(huì)說,這還不簡(jiǎn)單,把maxWorkerThreads調(diào)20不就行了么? 其實(shí)我們做也沒有什么 問題,確實(shí)是可以的,而且也確實(shí)能起到作用。那我們?yōu)槭裁催€要大費(fèi)周章的搞什么 async/await呢?搞得腦子都暈了?async/await給我們解決了什么問題?它可以在我們?cè)L問web service的時(shí)候把當(dāng)前的worker線程放走,將它放回線程池,這樣它就可以去處理其它的請(qǐng)求了。和IO線程一樣,IO線程只負(fù)責(zé)把請(qǐng)求交給Worker線程或者放入進(jìn)程池級(jí)別的隊(duì)列,然后又去HTTP.SYS的隊(duì)列中處理其它的請(qǐng)求。等到web service給我們返回結(jié)果了,會(huì)再到線程池中隨機(jī)拿一個(gè)新的woker線程繼續(xù)往下執(zhí)行。也就是說我們減少了那一部分等待的時(shí)間,充份利用了線程。
我們來對(duì)比一下使用async/awit和不使用的情況,
不使用async/await: 20個(gè)woker線程1s可以處理200個(gè)請(qǐng)求。
那轉(zhuǎn)換成總的時(shí)間的就是 20 * 1000ms = 20000ms,
其中等待的時(shí)間為 200 * 80ms = 16000ms。
也就是說使用async/await我們至少節(jié)約了16000ms的時(shí)間,這20個(gè)worker線程又會(huì)再去處理請(qǐng)求,即使按照每個(gè)請(qǐng)求100ms的處理時(shí)間我們還可以再增加160個(gè)請(qǐng)求。而且別忘了100ms是基于同步情況下,包括等待時(shí)間在內(nèi)的基礎(chǔ)上得到的,所以實(shí)際情況可能還要多,當(dāng)然我們這里沒有算上線程切換的時(shí)間,所以實(shí)際情況中是有一點(diǎn)差異的,但是應(yīng)該不會(huì)很大,因?yàn)槲覀兊木€程都是基于線程池的操作。
所有結(jié)果是20個(gè)Worker線程不使用異步的情況下,1s能自理200個(gè)請(qǐng)求,而使用異步的情況下可以處理360個(gè)請(qǐng)求,立馬提升80%呀!采用異步之后,對(duì)于同樣的請(qǐng)求數(shù)量,需要的Worker線程數(shù)據(jù)會(huì)大大減少50%左右,一個(gè)線程至少會(huì)在堆上分配1M的內(nèi)存,如果是1000個(gè)線程那就是1G的容量,雖然內(nèi)存現(xiàn)在便宜,但是省著總結(jié)是好的嘛,而且更少的線程是可以減少線程池在維護(hù)線程時(shí)產(chǎn)生的CPU消耗的。
注意:以上數(shù)據(jù)并非真實(shí)測(cè)試數(shù)據(jù),真實(shí)情況一個(gè)request的時(shí)間也并非100ms,花費(fèi)在web service上的時(shí)間也并非80ms,僅僅是給大家一個(gè)思路:),所以這里面用了async和await之后對(duì)響應(yīng)能力有多大的提升和我們?cè)瓉矶氯谶@些IO和網(wǎng)絡(luò)上的時(shí)間是有很大的關(guān)系的。
看到這里,不知道大家有沒有得到點(diǎn)什么。首先***點(diǎn)我們要知道的是async/await不是***藥,不們不能指望光寫兩個(gè)光鍵字就希望性能的提升。要記住,一個(gè)CPU在同一時(shí)間段內(nèi)是只能執(zhí)行一個(gè)線程的。所以這也是為什么async和await建議在IO或者是網(wǎng)絡(luò)操作的時(shí)候使用。我們的MVC站點(diǎn)訪問WCF或者Web Service這種場(chǎng)景就非常的適合使用異步來操作。在上面的例子中80ms讀取web service的時(shí)間,大部份時(shí)間都是不需要cpu操作的,這樣cpu才可以被其它的線程利用,如果不是一個(gè)讀取web service的操作,而是一個(gè)復(fù)雜計(jì)算的操作,那你就等著cpu爆表吧。
第二點(diǎn)是,除了程序中利用異步,我們上面講到的關(guān)于IIS的配置是很重要的,如果使用了異步,請(qǐng)記得把maxWorkerThreads和maxConcurrentRequestPerCPU的值調(diào)高試試。
講完我們高大上的async/await之后,我們來看看這個(gè)技術(shù)很老,但是概念確依舊延續(xù)至今的Web Service。 我們這里所說的針對(duì)web service的異步編程模式不是指在服務(wù)器端的web service本身,而是指調(diào)用web service的客戶端。大家知道對(duì)于web service,我們通過添加web service引用或者.net提供的生成工具就可以生成相應(yīng)的代理類,可以讓我們像調(diào)用本地代碼一樣訪問web service,而所生成的代碼類中對(duì)針對(duì)每一個(gè)web service方法生成3個(gè)對(duì)應(yīng)的方法,比如說我們的方法名叫DownloadContent,除了這個(gè)方法之外還有BeginDownloadContent和EndDownloadContent方法,而這兩個(gè)就是我們今天要說的早期的異步編程模式APM(Asynchronous Programming Model)。下面就來看看我們web service中的代碼,注意我們現(xiàn)在的項(xiàng)目都是在.NET Framework3.5下實(shí)現(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ù)器是同步還是異步,對(duì)于客戶端來說調(diào)用了你這個(gè)web service都是一樣的,就是得等你返回結(jié)果。
當(dāng)然,我們也可以像MVC里面的代碼一樣,把我們的服務(wù)器端也寫成異步的。那得到好處的是那個(gè)托管web service的服務(wù)器,它的處理能力得到提高,就像ASP.NET一樣。如果我們用JavaScript去調(diào)用這個(gè)Web Service,那么Ajax(Asynchronous Javascript + XML)就是我們客戶端用到的異步編程技術(shù)。如果是其它的客戶端呢?比如說一個(gè)CS的桌面程序?我們需要異步編程么?
WinForm不像托管在IIS的ASP.NET網(wǎng)站,會(huì)有一個(gè)線程池管理著多個(gè)線程來處理用戶的請(qǐng)求,換個(gè)說法ASP.NET網(wǎng)站生來就是基于多線程的。但是,在WinForm中,如果我們不刻意使用多線程,那至始至終,都只有一個(gè)線程,稱之為UI線程。也許在一些小型的系統(tǒng)中WinForm很少涉及到多線程,因?yàn)閃inForm本身的優(yōu)勢(shì)就在它是獨(dú)立運(yùn)行在客戶端的,在性能上和可操作性上都會(huì)有很大的優(yōu)勢(shì)。所以很多中小型的WinForm系統(tǒng)都是直接就訪問數(shù)據(jù)庫了,并且基本上也只有數(shù)據(jù)的傳輸,什么圖片資源那是很少的,所以等待的時(shí)間是很短的,基本不用費(fèi)什么腦力去考慮什么3秒之內(nèi)必須將頁面顯示到用戶面前這種問題。
既然WinForm在性能上有這么大的優(yōu)勢(shì),那它還需要異步嗎?
我們上面說的是中小型的WinForm,如果是大型的系統(tǒng)呢?如果WinForm只是其它的很小一部分,就像我們文章開始說的還有很多其它成千上萬個(gè)手機(jī)客戶端,Web客戶端,平板客戶端呢?如果客戶端很多導(dǎo)致數(shù)據(jù)庫撐不住怎么辦? 想在中間加一層緩存怎么辦?
拿一個(gè)b2b的網(wǎng)站功能舉例,用戶可以通過網(wǎng)站下單,手機(jī)也可以下單,還可以通過電腦的桌面客戶端下單。在下完單之后要完成交易,庫存扣減,發(fā)送訂單確認(rèn)通知等等功能,而不管你的訂單是通過哪個(gè)端完成的,這些功能我們都要去做,對(duì)嗎?那我們就不能單獨(dú)放在WinForm里面了,不然這些代碼在其它的端里面又得全部全新再一一實(shí)現(xiàn),同樣的代碼放在不同的地方那可是相當(dāng)危險(xiǎn)的,所以就有了我們后來的SOA架構(gòu),把這些功能都抽成服務(wù),每種類型的端都是調(diào)用服務(wù)就可以了。一是可以統(tǒng)一維護(hù)這些功能,二是可以很方便的做擴(kuò)展,去更好的適應(yīng)功能和架構(gòu)上的擴(kuò)展。比如說像下面這樣的一個(gè)系統(tǒng)。
在上圖中,Web端雖然也是屬于我們平常說的服務(wù)端(甚至是由多臺(tái)服務(wù)器組成的web群集),但是對(duì)我們整個(gè)系統(tǒng)來說,它也只是一個(gè)端而已。對(duì)于一個(gè)端來說,它本身只處理和用戶交互的問題,其余所有的功能,業(yè)務(wù)都會(huì)交給后來臺(tái)處理。在我們上面的架構(gòu)中,應(yīng)用層都不會(huì)直接參加真正業(yè)務(wù)邏輯相關(guān)的處理,而是放到我們更下層數(shù)據(jù)層去做處理。那么應(yīng)用層主要協(xié)助做一些與用戶交互的一些功能,如果手機(jī)短信發(fā)送,郵件發(fā)送等等,并且可以根據(jù)優(yōu)先級(jí)選擇是放入隊(duì)列中稍候處理還是直接調(diào)用功能服務(wù)立即處理。
在這樣的一個(gè)系統(tǒng)中,我們的Web服務(wù)器也好,Winform端也好都將只是整個(gè)系統(tǒng)中的一個(gè)終端,它們主要的任何是用戶和后面服務(wù)之間的一個(gè)橋梁。涉及到Service的調(diào)用之后,為了給用戶良好的用戶體驗(yàn),在WinForm端,我們自然就要考慮異步的問題。
有了像VS這樣強(qiáng)大的工具為我們生成代理類,我們?cè)趯懻{(diào)用Web service的代碼時(shí)就可以像調(diào)用本地類庫一樣調(diào)用Web Service了,我們只需要添加一個(gè)Web Reference就可以了。
// 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); }
代碼非常的簡(jiǎn)單,在執(zhí)行完pageContentService.BeginDownloadContent之后,我們的主線程就返回了。在調(diào)用Web service這段時(shí)間內(nèi)我們的UI不會(huì)被阻塞,也不會(huì)出現(xiàn)“無法響應(yīng)這種情況”,我們依然可以拖動(dòng)窗體甚至做其它的事情。這就是APM的魔力,但是我們的callback究竟是在哪個(gè)線程中執(zhí)行的呢?是線程池中的線程么?咋們接著往下看。
接下來我們就是更進(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); }
我們?cè)诎粹o點(diǎn)擊的方法和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>...
按鈕點(diǎn)擊的方法是由UI直接控制,很明顯它不是一個(gè)線程池線程,也不是后臺(tái)線程。而我們的callback卻是在一個(gè)來自于線程池的后臺(tái)線程執(zhí)行的,答案揭曉了,可是這會(huì)給我們帶來一個(gè)問題,我們上面講了只有UI線程也可以去更新我們的UI控件,也就是說在callback中我們是不能更新UI控件的,那我們?nèi)绾巫尭耈I讓用戶知道反饋呢?答案在后面接曉 :),讓我們先專注于把APM弄清楚。
其實(shí),APM在.NET3.5以前都被廣泛使用,在WinForm窗體控制中,在一個(gè)IO操作的類庫中等等!大家可以很容易的找到搭配了Begin和End的方法,更重要的是只要是有代理的地方,我們都可以使用APM這種模式。我們來看一個(gè)很簡(jiǎn)單的例子:
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封裝成一個(gè)委托,然后再調(diào)用該委托的BeginInvoke方法實(shí)現(xiàn)了異步的執(zhí)行。也就是實(shí)際的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,該方法會(huì)在我們的eat方法執(zhí)行完之后被調(diào)用,所以它會(huì)和我們eat方法在同一個(gè)線程中被調(diào)用。大家如果熟悉代理的話就會(huì)知道,代碼實(shí)際上會(huì)被編譯成一個(gè)類,而BeginInvoke和EndInvoke方法正是編譯器為我們自動(dòng)加進(jìn)去的方法,我們不用額外做任何事情,這在早期沒有TPL和async/await之前(APM從.NET1.0時(shí)代就有了),的確是一個(gè)不錯(cuò)的選擇。
了解了Delegate實(shí)現(xiàn)的BeginInvoke和EndInvoke之后,我們?cè)賮矸治鲆幌翧PM用到的那些對(duì)象。 拿我們Web service的代理類來舉例,它為我們生成了以下3個(gè)方法:
string DownloadContent(string url): 同步方法
IAsyncResult BeginDownloadContent(string url, AsyncCallback callback, object asyncState): 異步開始方法
EndDownloadContent(IAsyncResult asyncResult):異步結(jié)束方法
在我們調(diào)用EndDownloadContent方法的時(shí)候,如果我們的web service調(diào)用還沒有返回,那這個(gè)時(shí)候就會(huì)用阻塞的方式去拿結(jié)果。但是在我們傳到BeginDownloadContent中的callback被調(diào)用的時(shí)候,那操作一定是已經(jīng)完成了,也就是說IAsyncResult.IsCompleted = true。而在APM異步編程模式中Begin方法總是返回IAsyncResult這個(gè)接口的實(shí)現(xiàn)。IAsyncReuslt僅僅包含以下4個(gè)屬性:
WaitHanlde通常作為同步對(duì)象的基類,并且可以利用它來阻塞線程,更多信息可以參考MSDN 。 借助于IAsyncResult的幫助,我們就可以通過以下幾種方式去獲取當(dāng)同所執(zhí)行操作的結(jié)果。
輪詢
強(qiáng)制等待
完成通知
完成通知就是們上面用到的那種,調(diào)完Begin方法之后,主線程就算完成任務(wù)了。我們也不用監(jiān)控該操作的執(zhí)行情況,當(dāng)該操作執(zhí)行完之后,我們?cè)贐egin方法中傳進(jìn)去的callback就會(huì)被調(diào)用了,我們可以在那個(gè)方法中調(diào)用End方法去獲取結(jié)果。下面我們?cè)俸?jiǎn)單說一下前面兩種方式。
//輪詢獲取結(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)制等待時(shí)間 if (asyncResult.AsyncWaitHandle.WaitOne(2000)) { var content = pageContentService.EndDownloadContent(asyncResult); } else { // 2s時(shí)間已經(jīng)過了,但是還沒有執(zhí)行完 }
EAP是在.NET2.0推出的另一種過渡的異步編程模型,也是在.NET3.5以后Microsoft支持的一種做法,為什么呢? 如果大家建一個(gè).NET4.0或者更高版本的WinForm項(xiàng)目,再去添加Web Reference就會(huì)發(fā)現(xiàn)生成的代理類中已經(jīng)沒有Begin和End方法了,記住在3.5的時(shí)候是兩者共存的,你可以選擇任意一種來使用。但是到了.NET4.0以后,EAP成為了你唯一的選擇。(我沒有嘗試過手動(dòng)生成代理類,有興趣的同學(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 { // 出錯(cuò)了 } }
不知道大家還是否記得,在APM模式中,callback是執(zhí)行在另一個(gè)線程中,不能隨易的去更新UI。但是如果你仔細(xì)看一下上面的代碼,我們的DownloadContentCompleted事件綁定的方法中直接就更新了UI,把返回的內(nèi)容寫到了一個(gè)文本框里面。通過同樣的方法可以發(fā)現(xiàn),在EAP這種異步編程模式下,事件綁定的方法也是在調(diào)用的那個(gè)線程中執(zhí)行的。也就是說解決了異步編程的時(shí)候UI交互的問題,而且是在同一個(gè)線程中執(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給ASP.NET帶來的是處理能力的提高,那么在WinForm中給程序員帶來的好處則是***的。我們?cè)僖膊挥靡驗(yàn)橐獙?shí)現(xiàn)異步寫回調(diào)或者綁定事件了,省事了,可讀性也提高了。不信你看下面我們將調(diào)用我們那個(gè)web service的代碼在.NET4.5下實(shí)現(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; }
簡(jiǎn)單的三行代碼,像寫同步代碼一樣寫異步代碼,我想也許這就是async/await的魔力吧。在await之后,UI線程就可以回去響應(yīng)UI了,在上面的代碼中我們是沒有新線程產(chǎn)生的,和EAP一樣拿到結(jié)果直接就可以對(duì)UI操作了。
async/await似乎真的很好,但是如果我們await后面的代碼執(zhí)行在另外一個(gè)線程中會(huì)發(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; }
我們?cè)诮缑嬷蟹帕艘粋€(gè)ProgressBar,同時(shí)開一個(gè)線程去把從1到5000000的平方全部加起來,看起來是一個(gè)非常耗時(shí)的操作,于是我們用Task.Run開了一個(gè)新的線程去執(zhí)行。(注:如果是純運(yùn)算的操作,多線程操作對(duì)性能沒有多大幫助,我們這里主要是想給UI一個(gè)進(jìn)度顯示當(dāng)前進(jìn)行到哪一步了。)看起來沒有什么問題,我們按F5運(yùn)行吧!
Bomb~
當(dāng)執(zhí)行到這里的時(shí)候,程序就崩潰了,告訴我們”無效操作,只能從創(chuàng)建porgressBar的線程訪問它。“ 這也是我們一開始提到的,在WinForm程序中,只有UI主線程才能對(duì)UI進(jìn)行操作,其它的線程是沒有權(quán)限的。接下來我們就來看看,如果在WinForm中實(shí)現(xiàn)非UI線程對(duì)UI控制的更新操作。
WinForm中絕大多數(shù)的控件包括窗體在內(nèi)都實(shí)現(xiàn)了Invoke方法,可以傳入一個(gè)Delegate,這個(gè)Delegate將會(huì)被擁有那個(gè)控制的線程所調(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方法比較簡(jiǎn)單,我們就不做過多的研究了,但是我們要考慮到一點(diǎn),Invoke是WinForm實(shí)現(xiàn)的UI同交互技術(shù),WPF用的卻是Dispatcher,如果是在ASP.NET下跨線程之間的同步又怎么辦呢。為了兼容各種技術(shù)平臺(tái)下,跨線程同步的問題,Microsoft在.NET2.0的時(shí)候就引入了我們下面的這個(gè)對(duì)象。
為什么需要SynchronizationContext
就像我們?cè)赪inForm中遇到的問題一樣,有時(shí)候我們需要在一個(gè)線程中傳遞一些數(shù)據(jù)或者做一些操作到另一個(gè)線程。但是在絕大多數(shù)情況下這是不允許的,出于安全因素的考慮,每一個(gè)線程都有它獨(dú)立的內(nèi)存空間和上下文。因此在.NET2.0,微軟推出了SynchronizationContext。
它主要的功能之一是為我們提供了一種將一些工作任務(wù)(Delegate)以隊(duì)列的方式存儲(chǔ)在一個(gè)上下文對(duì)象中,然后把這些上下文對(duì)象關(guān)聯(lián)到具體的線程上,當(dāng)然有時(shí)候多個(gè)線程也可以關(guān)聯(lián)到同一個(gè)SynchronizationContext對(duì)象。獲取當(dāng)前線程的同步上下文對(duì)象可以使用SynchronizationContext.Current。同時(shí)它還為我們提供以下兩個(gè)方法Post和Send,分別是以異步和同步的方法將我們上面說的工作任務(wù)放到我們SynchronizationContext的隊(duì)列中。
SynchronizationContext示例
還是拿我們上面Invoke中用到的例子舉例,只是這次我們不直接調(diào)用控件的Invoke方法去更新它,而是寫了一個(gè)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)前正在計(jì)算的數(shù)字傳給它就可以了。接下來就看我們的Report方法了。
private SynchronizationContext m_SynchronizationContext; private DateTime m_PreviousTime = DateTime.Now; public Form1() { InitializeComponent(); // 在全局保存當(dāng)前UI線程的SynchronizationContext對(duì)象 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; } }
整個(gè)操作看起來要比Inovke復(fù)雜一點(diǎn),與Invoke不同的是SynchronizationContext不需要對(duì)Control的引用,而Invoke必須先得有那個(gè)控件才能調(diào)用它的Invoke方法去更新它。
這篇博客內(nèi)容有點(diǎn)長(zhǎng),不知道有多少人可以看到這里:)。最開始我只是想寫寫WinFrom下異步調(diào)用Web Service的一些東西,在我開始這篇文件的題目是”異步編程在WinForm下的實(shí)踐“,但是寫著寫著發(fā)現(xiàn)越來越多的迷團(tuán)沒有解開,其實(shí)都是一些老的技術(shù)以前沒有接觸和掌握好,所以所幸就一次性把他們都重新學(xué)習(xí)了一遍,與大家分享。
我們?cè)賮砘仡櫼幌挛恼滤婕暗降囊恍┲匾母拍睿?/p>
async/await 在ASP.NET做的***貢獻(xiàn)(早期ASP.NET的異步開發(fā)模式同樣也有這樣的貢獻(xiàn)),是在訪問數(shù)據(jù)庫的時(shí)候、訪問遠(yuǎn)程IO的時(shí)候及時(shí)釋放了當(dāng)前的處理性程,可以讓這些線程回到線程池中,從而實(shí)現(xiàn)可以去處理其它請(qǐng)求的功能。
異步的ASP.NET開發(fā)能夠在處理能力上帶來多大的提高,取決于我們的程序有多少時(shí)間是被阻塞的,也就是那些訪問數(shù)據(jù)庫和遠(yuǎn)程Service的時(shí)間。
除了將代碼改成異步,我們還需要在IIS上做一些相對(duì)的配置來實(shí)現(xiàn)***化。
不管是ASP.NET、WinForm還是Mobile、還是平板,在大型系統(tǒng)中都只是一個(gè)與用戶交互的端而已,所以不管你現(xiàn)在是做所謂的前端(JavaScript + CSS等),還是所謂的后端(ASP.NET MVC、WCF、Web API 等 ),又或者是比較時(shí)髦的移動(dòng)端(IOS也好,Andrioid也罷,哪怕是不爭(zhēng)氣的WP),都只是整個(gè)大型系統(tǒng)中的零星一角而已。當(dāng)然我并不是貶低這些端的價(jià)值,正是因?yàn)槲覀儗W⒂诓煌μ岣呙恳粋€(gè)端的用戶體驗(yàn),才能讓這些大型系統(tǒng)有露臉的機(jī)會(huì)。我想說的是,在你對(duì)現(xiàn)在技術(shù)取得一定的成就之后,不要停止學(xué)習(xí),因?yàn)檎麄€(gè)軟件架構(gòu)體系中還有很多很多美妙的東西值得我們?nèi)グl(fā)現(xiàn)。
APM和EAP是在async/await之前的兩種不同的異步編程模式。
APM如果不阻塞主線程,那么完成通知(回調(diào))就會(huì)執(zhí)行在另外一個(gè)線程中,從而給我們更新UI帶來一定的問題。
EAP的通知事件是在主線程中執(zhí)行的,不會(huì)存在UI交互的問題。
***,我們還學(xué)習(xí)了在Winform下不同線程之間交互的問題,以及SynchronizationContext。
APM是.NET下最早的異步編程方法,從.NET1.0以來就有了。在.NET2.0的時(shí)候,微軟意識(shí)到了APM的回調(diào)函數(shù)中與UI交互的問題,于是帶來了新的EAP。APM與EAP一直共存到.NET3.5,在.NET4.0的時(shí)候微軟帶來了TPL,也就是我們所熟知的Task編程,而.NET4.5就是我們大家知道的async/await了,可以看到.NET一直在不停的進(jìn)步,加上最近不斷的和開源社區(qū)的合作,跨平臺(tái)等特性的引入,我們有理由相信.NET會(huì)越走越好。
看完上述內(nèi)容,你們掌握如何解析異步編程In .NET APM/EAP和async/await的方法了嗎?如果還想學(xué)到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝各位的閱讀!
網(wǎng)站名稱:如何解析異步編程In.NETAPM/EAP和async/await
網(wǎng)頁網(wǎng)址:http://vcdvsql.cn/article4/pdhiie.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供定制開發(fā)、軟件開發(fā)、定制網(wǎng)站、商城網(wǎng)站、網(wǎng)站營(yíng)銷、服務(wù)器托管
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)