異步編程 In .NET(轉)

轉自:http://www.cnblogs.com/jesse2013/p/Asynchronous-Programming-In-DotNet.html

概述

  在以前寫的一篇關於async和await的前世此生的文章以後,你們彷佛在async和await提升網站處理能力方面還有一些疑問,博客園自己也作了很多的嘗試。今天咱們再來回答一下這個問題,同時咱們會作一個async和await在WinForm中的嘗試,而且對比在4.5以前的異步編程模式APM/EAP和async/await的區別,最後咱們還會探討在不一樣線程之間交互的問題。html

  IIS存在着處理能力的問題,可是WinForm倒是UI響應的問題,而且WinForm的UI線程至始至終都是同一個,因此二者之間有必定的區別。有人會問,如今還有人寫WinForm嗎?好吧,它確是一個比較老的東西呢,不如WPF炫,技術也不如WPF先進,可是從架構層面來說,無論是Web,仍是WinForm,又或是WPF,Mobile,這些都只是表現層,不是麼?如今的大型系統通常桌面客戶端,Web端,手機,平板端都會涉及,這也是爲何會有應用層,服務層的存在。咱們在這談論的ASP.NET MVC,WinForm,WFP,Android/IOS/WP 都是表現層,在表現層咱們應該只處理與「表現」相關的邏輯,任何與業務相關的邏輯應該都是放在下層處理的。關於架構的問題,咱們後面再慢慢深刻,另外別說我沒有提示您,咱們今天還會看到.NET中另外一個已經老去的技術Web Service。前端

  還得提示您,文章內容有點長,涉及的知識點比較多,因此,我推薦:」先頂後看「 ,先頂後看是21世紀看長篇的首選之道,是良好溝通的開端,想知道是什麼會讓你不同凡響嗎?想知道爲何上海今天會下這麼大的雨麼?請記住先頂後看,你頂的不是個人文章,而是咱們冒着大雨還要去上班的難得精神!先頂後看,你值得擁有!git

目錄

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

  首先響應能力並不徹底是說咱們程序性能的問題,有時候可能你的程序沒有任何問題,並且精心通過優化,但是響應能力仍是沒有上去,網站性能分析是一個複雜的活,有時候只能靠經驗和不斷的嘗試才能達到比較好的效果。固然咱們今天討論的主要是IIS的處理能力,或者也可能說是IIS的性能,但絕非代碼自己的性能。即便async/await可以提升IIS的處理能力,可是對於用戶來講整個頁面從發起請求到頁面渲染完成的這些時間,是不會由於咱們加了async/await以後產生多大變化的。程序員

  另外異步的ASP.NET並不是只有async/await才能夠作的,ASP.NET在Web Form時代就已經有異步Page了,包括ASP.NET MVC不是也有異步的Controller麼?async/await 很新,很酷,可是它也只是在原有一技術基礎上作了一些改進,讓程序員們寫起異步代碼來更容易了。你們常說微軟喜歡新瓶裝舊酒,至少咱們要看到這個新瓶給咱們帶來了什麼,無論是任何產品,都不可能一開始就很完美,因此不斷的迭代更新,也能夠說是一種正確作事的方式。web

ASP.NET並行處理的步驟

   ASP.NET是如何在IIS中工做的一文已經很詳細的介紹了一個請求是如何從客戶端到服務器的HTTP.SYS最後進入CLR進行處理的(強烈建議不瞭解這一塊的同窗先看這篇文章,有助於你理解本小節),可是全部的步驟都是基於一個線程的假設下進行的。IIS自己就是一個多線程的工做環境,若是咱們從多線程的視角來看會發生什麼變化呢?咱們首先來看一下下面這張圖。注意:咱們下面的步驟是創建在IIS7.0之後的集成模式基礎之上的。(注:下面這張圖在dudu的提醒以後,從新作了一些搜索工做,作了一些改動,w3dt這一步來自於博客園團隊對問題的不斷探索,詳情能夠點這裏數據庫

  咱們再來梳理一下上面的步驟:編程

  1. 全部的請求最開始是由HTTP.SYS接收的,HTTP.SYS內部有一個隊列維護着這些請求,這個隊列的request的數量大於必定數量(默認是1000)的時候,HTTP.SYS就會直接返回503狀態(服務器忙),這是咱們的第一個閥門。性能計數指標:「Http Service Request Queues\CurrentQueueSize
  2. 由w3dt負責把請求從HTTP.SYS 的隊列中放到一個對應端口的隊列中,據非官方資料顯示該隊列長度爲能爲20(該隊列是非公開的,沒有文檔,因此也沒有性能計數器)。
  3. IIS 的IO線程從上一步的隊列中獲取請求,若是是須要ASP.NET處理的,就會轉交給CLR 線程池的Worker 線程,IIS的IO線程繼續返回重複作該步驟。CLR 線程池的Worker線程數量是第二個閥門。
  4. 當CLR中正在被處理的請求數據大於必定值(最大並行處理請求數量,.NET4之後默認是5000)的時候,從IO線程過來的請求就不會直接交給Worker線程,而是放到一個進程池級別的一個隊列了,等到這個數量小於臨界值的時候,纔會把它再次交給Worker線程去處理。這是咱們的第三個閥門。
  5. 上一步中說到的那個進程池級別的隊列有一個長度的限制,能夠經過web.config裏面的processModel/requestQueueLimit來設置。這能夠說也是一個閥門。當正在處理的數量大於所容許的最大並行處理請求數量的時候,咱們就會獲得503了。能夠經過性能計數指標:「ASP.NET v4.0.30319\Requests Queued」 來查看該隊列的長度。

 哪些因素會控制咱們的響應能力

  從上面咱們提到了幾大閥門中,咱們能夠得出下面的幾個數字控制或者說影響着咱們的響應能力。c#

  1. HTTP.SYS隊列的長度
  2. CLR線程池最大Worker線程數量
  3. 最大並行處理請求數量
  4. 進程池級別隊列所容許的長度

HTTP.SYS隊列的長度後端

  這個我以爲不須要額外解釋,默認值是1000。這個值取決於咱們咱們後面IIS IO線程和Worker線程的處理速度,若是它們兩個都處理不了,這個數字再大也沒有用。由於最後他們會被存儲到進程池級別的隊列中,因此只會形成內存的浪費。緩存

最大Worker線程數量

  這個值是能夠在web.config中進行配置的。

  maxWorkerThreads: CLR中真實處理請求的最大Worker線程數量
  minWorkerThreads:CLR中真實處理請求的最小Worker線程數量

  minWorkerThreads的默認值是1,合理的加大他們能夠避免沒必要要的線程建立和銷燬工做。

最大並行處理請求數量

  進程池級別的隊列給咱們的CLR必定的緩衝,這裏面要注意的是,這個隊列尚未進入到CLR,因此它不會佔用咱們託管環境的任何資源,也就是把請求卡在了CLR的外面。咱們須要在aspnet.config級別進行配置,咱們能夠在.net fraemwork的安裝目錄下找到它。通常是 C:\Windows\Microsoft.NET\Framework\v4.0.30319 若是你安裝的是4.0的話。

  maxConcurrentRequestPerCPU: 每一個CPU所容許的最大並行處理請求數量,當CLR中worker線程正在處理的請求之和大於這個數時,從IO線程過來的請求就會被放到咱們進程池級別的隊列中。
  maxConcurrentThreadsPerCPU: 設置爲0即禁用。
  requestQueue: 進程池級別隊列所容許的長度  

async和await 作了什麼?

  咱們終於要切入正題了,拿ASP.NET MVC舉例,若是不採用async的Action,那麼毫無疑問,它是在一個Woker線程中執行的。當咱們訪問一些web service,或者讀文件的時候,這個Worker線程就會被阻塞。假設咱們這個Action執行時間一共是100ms,其它訪問web service花了80ms,理想狀況下一個Worker線程一秒能夠響應10個請求,假設咱們的maxWorkerThreads是10,那咱們一秒內老是可響應請求就是100。若是說咱們想把這個可響應請求數升到200怎麼作呢?

  有人會說,這還不簡單,把maxWorkerThreads調20不就好了麼? 其實咱們作也沒有什麼 問題,確實是能夠的,並且也確實能起到做用。那咱們爲何還要大費周章的搞什麼 async/await呢?搞得腦子都暈了?async/await給咱們解決了什麼問題?它能夠在咱們訪問web service的時候把當前的worker線程放走,將它放回線程池,這樣它就能夠去處理其它的請求了。等到web service給咱們返回結果了,會再到線程池中隨機拿一個新的woker線程繼續往下執行。也就是說咱們減小了那一部分等待的時間,充份利用了線程。

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

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

  那轉換成總的時間的就是 20 * 1000ms =  20000ms,
  其中等待的時間爲 200 * 80ms = 16000ms。
  也就是說使用async/await咱們至少節約了16000ms的時間,這20個worker線程又會再去處理請求,即便按照每一個請求100ms的處理時間咱們還能夠再增長160個請求。並且別忘了100ms是基於同步狀況下,包括等待時間在內的基礎上獲得的,因此實際狀況可能還要多,固然咱們這裏沒有算上線程切換的時間,因此實際狀況中是有一點差別的,可是應該不會很大,由於咱們的線程都是基於線程池的操做。
  全部結果是20個Worker線程不使用異步的狀況下,1s能自理200個請求,而使用異步的狀況下能夠處理360個請求,立馬提高80%呀!採用異步以後,對於一樣的請求數量,須要的Worker線程數據會大大減小50%左右,一個線程至少會在堆上分配1M的內存,若是是1000個線程那就是1G的容量,雖然內存如今便宜,可是省着總歸是好的嘛,並且更少的線程是能夠減小線程池在維護線程時產生的CPU消耗的。另:dudu分享 CLR1秒以內只能建立2個線程。

  注意:以上數據並不是真實測試數據,真實狀況一個request的時間也並不是100ms,花費在web service上的時間也並不是80ms,僅僅是給你們一個思路:),因此這裏面用了async和await以後對響應能力有多大的提高和咱們原來堵塞在這些IO和網絡上的時間是有很大的關係的。

幾點建議

  看到這裏,不知道你們有沒有獲得點什麼。首先第一點咱們要知道的是async/await不是萬能藥,不們不能期望光寫兩個光鍵字就但願性能的提高。要記住,一個CPU在同一時間段內是隻能執行一個線程的。因此這也是爲何async和await建議在IO或者是網絡操做的時候使用。咱們的MVC站點訪問WCF或者Web Service這種場景就很是的適合使用異步來操做。在上面的例子中80ms讀取web service的時間,大部份時間都是不須要cpu操做的,這樣cpu才能夠被其它的線程利用,若是不是一個讀取web service的操做,而是一個複雜計算的操做,那你就等着cpu爆表吧。

  第二點是,除了程序中利用異步,咱們上面講到的關於IIS的配置是很重要的,若是使用了異步,請記得把maxWorkerThreads和maxConcurrentRequestPerCPU的值調高試試。

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

  講完咱們高大上的async/await以後,咱們來看看這個技術很老,可是概念確依舊延續至今的Web Service。 咱們這裏所說的針對web service的異步編程模式不是指在服務器端的web service自己,而是指調用web service的客戶端。你們知道對於web service,咱們經過添加web service引用或者.net提供的生成工具就能夠生成相應的代理類,可讓咱們像調用本地代碼同樣訪問web service,而所生成的代碼類中對針對每個web service方法生成3個對應的方法,好比說咱們的方法名叫DownloadContent,除了這個方法以外還有BeginDownloadContent和EndDownloadContent方法,而這兩個就是咱們今天要說的早期的異步編程模式APM(Asynchronous Programming Model)。下面就來看看咱們web service中的代碼,注意咱們如今的項目都是在.NET Framework3.5下實現的。

 PageContent.asmx的代碼

1
2
3
4
5
6
7
8
9
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方法調用的是WebClient的同步方法,WebClient也有異步方法即:DownloadStringAsync。可是你們要明白,無論服務器是同步仍是異步,對於客戶端來講調用了你這個web service都是同樣的,就是得等你返回結果。

  固然,咱們也能夠像MVC裏面的代碼同樣,把咱們的服務器端也寫成異步的。那獲得好處的是那個託管web service的服務器,它的處理能力獲得提升,就像ASP.NET同樣。若是咱們用JavaScript去調用這個Web Service,那麼Ajax(Asynchronous Javascript + XML)就是咱們客戶端用到的異步編程技術。若是是其它的客戶端呢?好比說一個CS的桌面程序?咱們須要異步編程麼?

當WinForm趕上Web Service

  WinForm不像託管在IIS的ASP.NET網站,會有一個線程池管理着多個線程來處理用戶的請求,換個說法ASP.NET網站生來就是基於多線程的。可是,在WinForm中,若是咱們不刻意使用多線程,那至始至終,都只有一個線程,稱之爲UI線程。也許在一些小型的系統中WinForm不多涉及到多線程,由於WinForm自己的優點就在它是獨立運行在客戶端的,在性能上和可操做性上都會有很大的優點。因此不少中小型的WinForm系統都是直接就訪問數據庫了,而且基本上也只有數據的傳輸,什麼圖片資源那是不多的,因此等待的時間是很短的,基本不用費什麼腦力去考慮什麼3秒以內必須將頁面顯示到用戶面前這種問題。

  既然WinForm在性能上有這麼大的優點,那它還須要異步嗎?

  咱們上面說的是中小型的WinForm,若是是大型的系統呢?若是WinForm只是其它的很小一部分,就像咱們文章開始說的還有不少其它成千上萬個手機客戶端,Web客戶端,平板客戶端呢?若是客戶端不少致使數據庫撐不住怎麼辦? 想在中間加一層緩存怎麼辦?

  拿一個b2b的網站功能舉例,用戶能夠經過網站下單,手機也能夠下單,還能夠經過電腦的桌面客戶端下單。在下完單以後要完成交易,庫存扣減,發送訂單確認通知等等功能,而無論你的訂單是經過哪一個端完成的,這些功能咱們都要去作,對嗎?那咱們就不能單獨放在WinForm裏面了,否則這些代碼在其它的端裏面又得所有全新再一一實現,一樣的代碼放在不一樣的地方那但是至關危險的,因此就有了咱們後來的SOA架構,把這些功能都抽成服務,每種類型的端都是調用服務就能夠了。一是能夠統一維護這些功能,二是能夠很方便的作擴展,去更好的適應功能和架構上的擴展。好比說像下面這樣的一個系統。

 

  在上圖中,Web端雖然也是屬於咱們日常說的服務端(甚至是由多臺服務器組成的web羣集),可是對咱們整個系統來講,它也只是一個端而已。對於一個端來講,它自己只處理和用戶交互的問題,其他全部的功能,業務都會交給後來臺處理。在咱們上面的架構中,應用層都不會直接參加真正業務邏輯相關的處理,而是放到咱們更下層數據層去作處理。那麼應用層主要協助作一些與用戶交互的一些功能,若是手機短信發送,郵件發送等等,而且能夠根據優先級選擇是放入隊列中稍候處理仍是直接調用功能服務當即處理。

  在這樣的一個系統中,咱們的Web服務器也好,Winform端也好都將只是整個系統中的一個終端,它們主要的任何是用戶和後面服務之間的一個橋樑。涉及到Service的調用以後,爲了給用戶良好的用戶體驗,在WinForm端,咱們天然就要考慮異步的問題。 

WinForm異步調用Web Service

  有了像VS這樣強大的工具爲咱們生成代理類,咱們在寫調用Web service的代碼時就能夠像調用本地類庫同樣調用Web Service了,咱們只須要添加一個Web Reference就能夠了。

// Form1.cs的代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);
}

  代碼很是的簡單,在執行完pageContentService.BeginDownloadContent以後,咱們的主線程就返回了。在調用Web service這段時間內咱們的UI不會被阻塞,也不會出現「沒法響應這種狀況」,咱們依然能夠拖動窗體甚至作其它的事情。這就是APM的魔力,可是咱們的callback到底是在哪一個線程中執行的呢?是線程池中的線程麼?咋們接着往下看。

APM異步編程模式詳解

線程問題

  接下來咱們就是更進一步的瞭解APM這種模式是如何工做的,可是首先咱們要回答上面留下來的問題,這種異步的編程方式有沒有爲咱們開啓新的線程?讓代碼說話:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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方法中分別輸出當前線程的ID,以及他們是否屬於線程池的線程,獲得的結果以下:

  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倒是在一個來自於線程池的後臺線程執行的,答案揭曉了,但是這會給咱們帶來一個問題,咱們上面講了只有UI線程也能夠去更新咱們的UI控件,也就是說在callback中咱們是不能更新UI控件的,那咱們如何讓更新UI讓用戶知道反饋呢?答案在後面接曉 :),讓咱們先專一於把APM弄清楚。

從Delegate開始

  其實,APM在.NET3.5之前都被普遍使用,在WinForm窗體控制中,在一個IO操做的類庫中等等!你們能夠很容易的找到搭配了Begin和End的方法,更重要的是隻要是有代理的地方,咱們均可以使用APM這種模式。咱們來看一個很簡單的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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封裝成一個委託,而後再調用該委託的BeginInvoke方法實現了異步的執行。也就是實際的eat方法不是在主線程中執行的,咱們能夠看輸出的結果:

  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是咱們傳進去的callback,該方法會在咱們的eat方法執行完以後被調用,因此它會和咱們eat方法在同一個線程中被調用。你們若是熟悉代理的話就會知道,代碼實際上會被編譯成一個類,而BeginInvoke和EndInvoke方法正是編譯器爲咱們自動加進去的方法,咱們不用額外作任何事情,這在早期沒有TPL和async/await以前(APM從.NET1.0時代就有了),的確是一個不錯的選擇。

再次認識APM

瞭解了Delegate實現的BeginInvoke和EndInvoke以後,咱們再來分析一下APM用到的那些對象。 拿咱們Web service的代理類來舉例,它爲咱們生成了如下3個方法:

  1. string DownloadContent(string url): 同步方法
  2. IAsyncResult BeginDownloadContent(string url, AsyncCallback callback, object asyncState): 異步開始方法
  3. EndDownloadContent(IAsyncResult asyncResult):異步結束方法

  在咱們調用EndDownloadContent方法的時候,若是咱們的web service調用尚未返回,那這個時候就會用阻塞的方式去拿結果。可是在咱們傳到BeginDownloadContent中的callback被調用的時候,那操做必定是已經完成了,也就是說IAsyncResult.IsCompleted = true。而在APM異步編程模式中Begin方法老是返回IAsyncResult這個接口的實現。IAsyncReuslt僅僅包含如下4個屬性:

  WaitHanlde一般做爲同步對象的基類,而且能夠利用它來阻塞線程,更多信息能夠參考MSDN 。 藉助於IAsyncResult的幫助,咱們就能夠經過如下幾種方式去獲取當前所執行操做的結果。

  1. 輪詢
  2. 強制等待
  3. 完成通知

  完成通知就是們在"WinForm異步調用WebService"那結中用到的方法,調完Begin方法以後,主線程就算完成任務了。咱們也不用監控該操做的執行狀況,當該操做執行完以後,咱們在Begin方法中傳進去的callback就會被調用了,咱們能夠在那個方法中調用End方法去獲取結果。下面咱們再簡單說一下前面兩種方式。

//輪詢獲取結果代碼

1
2
3
4
5
6
7
8
9
10
11
var  pageContentService =  new  localhost.PageContent();
IAsyncResult asyncResult = pageContentService.BeginDownloadContent(
     null ,
     pageContentService);
 
while  (!asyncResult.IsCompleted)
{
     Thread.Sleep(100);
}
var  content = pageContentService.EndDownloadContent(asyncResult);

 // 強制等待結果代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var  pageContentService =  new  localhost.PageContent();
IAsyncResult asyncResult = pageContentService.BeginDownloadContent(
     null ,
     pageContentService);
 
// 也能夠調用WaitOne()的無參版本,不限制強制等待時間
if  (asyncResult.AsyncWaitHandle.WaitOne(2000))
{
     var  content = pageContentService.EndDownloadContent(asyncResult);
}
else
{
     // 2s時間已通過了,可是尚未執行完  
}

EAP(Event-Based Asynchronous Pattern)

  EAP是在.NET2.0推出的另外一種過渡的異步編程模型,也是在.NET3.5之後Microsoft支持的一種作法,爲何呢? 若是你們建一個.NET4.0或者更高版本的WinForm項目,再去添加Web Reference就會發現生成的代理類中已經沒有Begin和End方法了,記住在3.5的時候是二者共存的,你能夠選擇任意一種來使用。可是到了.NET4.0之後,EAP成爲了你惟一的選擇。(我沒有嘗試過手動生成代理類,有興趣的同窗能夠嘗試一下)讓咱們來看一下在.NET4下,咱們是如何異步調用Web Service的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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是執行在另外一個線程中,不能隨易的去更新UI。可是若是你仔細看一下上面的代碼,咱們的DownloadContentCompleted事件綁定的方法中直接就更新了UI,把返回的內容寫到了一個文本框裏面。經過一樣的方法能夠發現,在EAP這種異步編程模式下,事件綁定的方法也是在調用的那個線程中執行的。也就是說解決了異步編程的時候UI交互的問題,並且是在同一個線程中執行。 看看下面的代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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中給程序員帶來的好處則是最大的。咱們不再用由於要實現異步寫回調或者綁定事件了,省事了,可讀性也提升了。不信你看下面咱們將調用咱們那個web service的代碼在.NET4.5下實現一下:

1
2
3
4
5
6
7
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線程就能夠回去響應UI了,在上面的代碼中咱們是沒有新線程產生的,和EAP同樣拿到結果直接就能夠對UI操做了。

  async/await彷佛真的很好,可是若是咱們await後面的代碼執行在另一個線程中會發生什麼事情呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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開了一個新的線程去執行。(注:若是是純運算的操做,多線程操做對性能沒有多大幫助,咱們這裏主要是想給UI一個進度顯示當前進行到哪一步了。)看起來沒有什麼問題,咱們按F5運行吧!
  Bomb~

  當執行到這裏的時候,程序就崩潰了,告訴咱們」無效操做,只能從建立porgressBar的線程訪問它。「  這也是咱們一開始提到的,在WinForm程序中,只有UI主線程才能對UI進行操做,其它的線程是沒有權限的。接下來咱們就來看看,若是在WinForm中實現非UI線程對UI控制的更新操做。 

不一樣線程之間通信的問題

萬能的Invoke

  WinForm中絕大多數的控件包括窗體在內都實現了Invoke方法,能夠傳入一個Delegate,這個Delegate將會被擁有那個控制的線程所調用,從而避免了跨線程訪問的問題。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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實現的UI跨線程溝通方式,WPF用的倒是Dispatcher,若是是在ASP.NET下跨線程之間的同步又怎麼辦呢。爲了兼容各類技術平臺下,跨線程同步的問題,Microsoft在.NET2.0的時候就引入了咱們下面的這個對象。

SynchronizationContext上下文同步對象

爲何須要SynchronizationContext

  就像咱們在WinForm中遇到的問題同樣,有時候咱們須要在一個線程中傳遞一些數據或者作一些操做到另外一個線程。可是在絕大多數狀況下這是不容許的,出於安全因素的考慮,每個線程都有它獨立的內存空間和上下文。所以在.NET2.0,微軟推出了SynchronizationContext。

  它主要的功能之一是爲咱們提供了一種將一些工做任務(Delegate)以隊列的方式存儲在一個上下文對象中,而後把這些上下文對象關聯到具體的線程上,固然有時候多個線程也能夠關聯到同一個SynchronizationContext對象。獲取當前線程的同步上下文對象可使用SynchronizationContext.Current。同時它還爲咱們提供如下兩個方法Post和Send,分別是以異步和同步的方法將咱們上面說的工做任務放到咱們SynchronizationContext的隊列中。

SynchronizationContext示例

  仍是拿咱們上面Invoke中用到的例子舉例,只是此次咱們不直接調用控件的Invoke方法去更新它,而是寫了一個Report的方法專門去更新UI。

1
2
3
4
5
6
7
8
9
10
11
12
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;
});

  每一次操做完以後咱們調用一下Report方法,把咱們總共要算的數字,以及當前正在計算的數字傳給它就能夠了。接下來就看咱們的Report方法了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private  SynchronizationContext m_SynchronizationContext;
private  DateTime m_PreviousTime = DateTime.Now;
 
public  Form1()
{
     InitializeComponent();
     // 在全局保存當前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複雜一點,與Invoke不一樣的是SynchronizationContext不須要對Control的引用,而Invoke必須先得有那個控件才能調用它的Invoke方法對它進行操做。

小結

  這篇博客內容有點長,不知道有多少人能夠看到這裏:)。最開始我只是想寫寫WinFrom下異步調用Web Service的一些東西,在一開始這篇文件的題目是」異步編程在WinForm下的實踐「,可是寫着寫着發現愈來愈多的迷團沒有解開,其實都是一些老的技術之前沒有接觸和掌握好,因此所幸就一次性把他們都從新學習了一遍,與你們分享。

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

  1. async/await 在ASP.NET作的最大貢獻(早期ASP.NET的異步開發模式一樣也有這樣的貢獻),是在訪問數據庫的時候、訪問遠程IO的時候及時釋放了當前的處理性程,可讓這些線程回到線程池中,從而實現能夠去處理其它請求的功能。
  2. 異步的ASP.NET開發可以在處理能力上帶來多大的提升,取決於咱們的程序有多少時間是被阻塞的,也就是那些訪問數據庫和遠程Service的時間。
  3. 除了將代碼改爲異步,咱們還須要在IIS上作一些相對的配置來實現最優化。
  4. 無論是ASP.NET、WinForm仍是Mobile、仍是平板,在大型系統中都只是一個與用戶交互的端而已,因此無論你如今是作所謂的前端(JavaScript + CSS等),仍是所謂的後端(ASP.NET MVC、WCF、Web API 等 ),又或者是比較時髦的移動端(IOS也好,Andrioid也罷,哪怕是不爭氣的WP),都只是整個大型系統中的零星一角而已。固然我並非貶低這些端的價值,正是由於咱們專一於不一樣,努力提升每個端的用戶體驗,才能讓這些大型系統有露臉的機會。我想說的是,在你對如今技術取得必定的成就以後,不要中止學習,由於整個軟件架構體系中還有不少不少美妙的東西值得咱們去發現。
  5. APM和EAP是在async/await以前的兩種不一樣的異步編程模式。
  6. APM若是不阻塞主線程,那麼完成通知(回調)就會執行在另一個線程中,從而給咱們更新UI帶來必定的問題。
  7. EAP的通知事件是在主線程中執行的,不會存在UI交互的問題。
  8. 最後,咱們還學習了在Winform下不一樣線程之間交互的問題,以及SynchronizationContext。
  9. APM是.NET下最先的異步編程方法,從.NET1.0以來就有了。在.NET2.0的時候,微軟意識到了APM的回調函數中與UI交互的問題,因而帶來了新的EAP。APM與EAP一直共存到.NET3.5,在.NET4.0的時候微軟帶來了TPL,也就是咱們所熟知的Task編程,而.NET4.5就是咱們你們知道的async/await了,能夠看到.NET一直在不停的進步,加上最近不斷的和開源社區的合做,跨平臺等特性的引入,咱們有理由相信.NET會越走越好。

  最後,這篇文章從找資料學習到寫出來,差很少花了我兩個周未的時間,但願可以給須要的人或者感興趣想要不斷學習的人一點幫助(無論是往前學習,仍是日後學習)最後還要感謝@田園裏面的蟋蟀,在閱讀的時候給我找了一些錯別字!

引用 & 擴展閱讀

http://blogs.msdn.com/b/tmarq/archive/2010/04/14/performing-asynchronous-work-or-tasks-in-asp-net-applications.aspx
http://blog.stevensanderson.com/2008/04/05/improve-scalability-in-aspnet-mvc-using-asynchronous-requests
http://blogs.msdn.com/b/tmarq/archive/2007/07/21/asp-net-thread-usage-on-iis-7-0-and-6-0.aspx 
http://blogs.msdn.com/b/tmarq/archive/2010/04/14/performing-asynchronous-work-or-tasks-in-asp-net-applications.aspx
http://mohamadhalabi.com/2014/05/08/thread-throttling-in-iis-hosted-wcf-sync-vs-async/
Pro Asynchronous Programs with .NET by Richard Blewett and Andrew Clymer

做者:Jesse 出處:  http://jesse2013.cnblogs.com/
本文版權歸做者和博客園共有,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,不然保留追究法律責任的權利。若是以爲還有幫助的話,能夠點一下右下角的 【推薦】,但願可以持續的爲你們帶來好的技術文章!想跟我一塊兒進步麼?那就 【關注】我吧。
 
分類:  C# 揭密
標籤:  c#.net framework架構異步編程
好文要頂  關注我  收藏該文   
390
2
 
推薦成功
 
« 上一篇: Windows平臺分佈式架構實踐 - 負載均衡(下)
» 下一篇: 初探領域驅動設計(1)爲複雜業務而生
posted @  2014-07-15 08:42 騰飛(Jesse) 閱讀(19808) 評論(152) 編輯 收藏
相關文章
相關標籤/搜索