WinForm二三事(二)非同步作業

來源:互聯網
上載者:User
監視訊息迴圈

在上一篇文章中,我們討論了訊息迴圈是響應使用者輸入的根本,還提到了在WinForm中執行耗時操作是因為這個耗時操作與訊息迴圈在同一個UI Thread上,導致不能處理使用者的後續響應,造成程式假死。除此之外,還說到了Form中的WndProc方法,說這個方法就是Win32時代那個處理訊息的方法的.Net版。

那麼今天這篇文章我們就來編個小程式來類比一下這個耗時操作,看看是不是如上一篇所說:耗時操作造成訊息迴圈的臨時中斷不能響應使用者後續輸入。

程式很簡單,就是一個簡單的表單,上面放置一個按鈕,按鈕裡有一個Thread.Sleep(50*1000)類比耗時操作:

public partial class LongTimeForm : Form
{
    public LongTimeForm()
    {
        InitializeComponent();
        Debug.Listeners.Add(new ConsoleTraceListener());
    }
 
    private void btnLongTime_Click(object sender, EventArgs e)
    {
        Thread.Sleep(50 * 1000);
    }
 
    //既然這個WndProc是Win32中處理訊息的方法的.Net版,那麼我們應該在這裡可以監視到所有使用者操作的“訊息”
    protected override void WndProc(ref Message m)
    {
        Debug.WriteLine(m.Msg.ToString());
        base.WndProc(ref m);
    }
}

WndProc是一個虛方法,我們可以override,那麼我們就來看看當使用者點擊“耗時操作”的按鈕後,這個方法是不是還能接收到使用者其他的輸入呢。

小技巧
在開發WinForm程式時,為了方便顯示程式的一些動作記錄,我們經常將項目屬性裡的“Windows Application”項目類型修改為“Console Application”,這樣在啟動程式後,除了會顯示表單外,還會顯示一個控制台,在控制台裡會顯示程式裡通過Debug.Write等輸出的日誌。當產品發布的時候,我們可以將項目屬性修改回”Windows Application”。

啟動程式,滑鼠在表單上滑動,後面的控制台上就會顯示很多的數字,這些都是訊息迴圈從訊息佇列裡取出的訊息(數字是訊息的類別,在Windows的一個標頭檔裡定義),然後傳遞給WndProc方法處理的,控制台上還能輸出數字說明現在訊息迴圈還是“流暢的”。當我們點擊“耗時操作”按鈕後,我們發現,表單這個時候“死掉了”,不能再接受使用者任何的操作,而不管滑鼠如何在表單上滑動、點擊,後面的控制台沒有一條輸出。唔,訊息迴圈阻塞了。 Thread.Sleep是50s時間,50s後程式又活過來了,控制台上又有源源不斷的輸出了。

雖然,我們用執行個體證明了上一篇所說的東西貌似是正確的,但是難道我們就對這種耗時操作無能為力了嗎?不啊,我們有多線程啊,我們有非同步作業啊。我們就來看看如何使用非同步作業來處理這種耗時操作。

使用委託中的BeginInvoke進行非同步作業

還記得委託的BeginInvoke方法和EndInvoke方法嗎?今兒我們就用這兩個方法來做一個非同步操作(.Net中如果你看到這種BeginXXX和EndXXX成對出現的方法,那說明就是可以進行非同步作業了)。

當你定義一個委託後,編譯器會自動的為你產生一個類,還會為你在這個類裡提供一個BeginInvoke方法和一個EndInvoke方法,這兩個方法的實現是由CLR提供的,而這個BeginInvoke和EndInvoke只是起一個封裝的作用。我們還是先來看看將上面的耗時操作修改為非同步代碼吧:

public partial class LongTimeForm : Form
{
    public LongTimeForm()
    {
        InitializeComponent();
        Debug.Listeners.Add(new ConsoleTraceListener());
    }
 
    private void LongTimeMethod()
    {
        Thread.Sleep(50 * 1000);
    }
 
    private void btnLongTime_Click(object sender, EventArgs e)
    {
        //咱們這兒就不自己定義新的委託了,.Net為我們定義了一串的通用委託使用
        Action longTimeAction = new Action(LongTimeMethod);
        longTimeAction.BeginInvoke(null, null);
    }
    protected override void WndProc(ref Message m)
    {
        Debug.WriteLine(m.Msg.ToString());
        base.WndProc(ref m);
    }
}

現在再來運行程式。哇塞,雖然“耗時操作”的按鈕點擊後,訊息迴圈繼續進行,介面也沒有假死。嗯,這才是我們要的使用者體驗(當然,我們應該還給使用者一些提示,說耗時操作進行中,請不要關閉視窗)。

接下來我們來看看這個非同步作業是咋實現的。首先,我們在LongTimeMethod方法裡設定斷點,點擊“耗時操作”按鈕後,等待命中斷點,斷點命中後,選擇Visual Studio的“Debug”->”Windows”->”Threads”,這樣會開啟線程的視窗,在這裡我們可以看到類似的圖片:

有箭頭指示的是正在執行的LongTimeMethod方法的Worker Thread,從這裡可以看出Main方法是Main Thread中,看來BeginInvoke就是從Thread Pool中選擇一個空閑線程來執行我們的耗時操作。我提到這裡是因為經常有人問我:非同步和多線程有什麼區別?其實,非同步是目的,而多線程是實現這個目的的方法。非同步是說,A發起一個操作後(一般都是比較耗時的操作,如果不耗時的操作就沒有必要非同步了),可以繼續自顧自的處理它自己的事兒,不用乾等著這個耗時操作返回。.Net中的這種非同步編程模型,就簡化了多線程編程,我們甚至都不用去關心Thread類,就可以做一個非同步作業出來。

去了還要回

實際上上面示範的耗時操作是“一去不複返”的操作(相當於WCF中的One-Way操作),也就是我發起這個操作後,我就不用管它了,我甚至不關心它運算的結果。但大部分時候我們需要這樣的操作:執行完後返回來更新一下UI,比如告訴使用者一聲我執行完了或者顯示執行結果。那這樣我們就要考慮非同步呼叫的幾種方式了。如果我們要從非同步作業裡擷取結果,我們就得調用EndInvoke方法,那我們又用什麼手段來得到非同步作業完成的訊號呢?因為如果非同步作業沒有完成,我們就直接調用EndInvoke方法,這樣就會阻塞,一直等到非同步作業執行完畢後才會執行。

在繼續討論之前我們來看看BeginInvoke的傳回值:

   1: public interface IAsyncResult
   2: {
   3:     object AsyncState { get; }
   4:  
   5:     WaitHandle AsyncWaitHandle { get; }
   6:  
   7:     bool CompletedSynchronously { get; }
   8:  
   9:     bool IsCompleted { get; }
  10: }

根據BeginInvoke返回的結果,我們就有兩種調用非同步作業的方式:

輪詢

IAsyncResult的IsCompleted屬性會在非同步作業結束後返回true,否則返回false。那麼我們就可以用一個迴圈不斷的訪問IsCompleted屬性,當IsCompleted為true的時候再調用EndInvoke方法:

   1: public partial class LongTimeForm : Form
   2: {
   3:     public LongTimeForm()
   4:     {
   5:         InitializeComponent();
   6:         Debug.Listeners.Add(new ConsoleTraceListener());
   7:     }
   8:  
   9:     private int LongTimeMethod()
  10:     {
  11:         Thread.Sleep(50 * 1000);
  12:         return 10;
  13:     }
  14:  
  15:     private void btnLongTime_Click(object sender, EventArgs e)
  16:     {
  17:         //咱們這兒就不自己定義新的委託了,.Net為我們定義了一串的通用委託使用
  18:         Func<int> longTimeAction = new Func<int>(LongTimeMethod);
  19:         IAsyncResult asynResult = longTimeAction.BeginInvoke(null, null);
  20:  
  21:         //可以做別的事情
  22:         while (!asynResult.IsCompleted)
  23:         { 
  24:             
  25:         }
  26:         int result = longTimeAction.EndInvoke(asynResult);
  27:  
  28:     }
  29:     protected override void WndProc(ref Message m)
  30:     {
  31:         Debug.WriteLine(m.Msg.ToString());
  32:         base.WndProc(ref m);
  33:     }
  34: }

WaitOne

在IAsyncResult裡還有一個AsyncWaitHandle屬性,這是一個WaitHandle類型的屬性,這個對象有一個WaitOne方法,還能接受一個逾時時間,它會等待這個逾時時間指定的長度:

   1: private int LongTimeMethod()
   2: {
   3:     Thread.Sleep(50 * 1000);
   4:     return 10;
   5: }
   6: private void btnLongTime_Click(object sender, EventArgs e)
   7: {
   8:     Func<int> longTimeAction = new Func<int>(LongTimeMethod);
   9:     IAsyncResult asynResult = longTimeAction.BeginInvoke(null, null);
  10:  
  11:     //可以繼續處理別的事情
  12:  
  13:     if (asynResult.AsyncWaitHandle.WaitOne(10000, true))
  14:     {
  15:         int result = longTimeAction.EndInvoke(asynResult);
  16:     }
  17: }

上面的代碼的意思就是,非同步呼叫耗時操作後,繼續幹自己的事兒,然後幹完自己的事兒再來等著一個訊號,啥訊號呢?就是這個耗時操作完成的訊號。而且您還別讓我等得太久,等久了我就不耐煩了(我可只等待10秒鐘啊)。暈死,上面這耗時操作就要執行50秒鐘,你就等10秒鐘,這不是玩我嗎(10秒鐘時間過去了,這個WaitOne就不再等待了,線程將繼續執行)。

回調

其實不管是上面使用輪詢的方式還是使用WaitOne等待一個訊號量,還是要等待。等待是個讓人很惱火的事情。.Net考慮了這一點,為我們準備了回調的方式:你非同步呼叫後繼續幹你的事兒,等你執行完後,你告我一聲就ok了。

   1: private void btnLongTime_Click(object sender, EventArgs e)
   2: {
   3:     Func<int> longTimeAction = new Func<int>(LongTimeMethod);
   4:     //這裡使用了一個lambda運算式,省了不少力啊
   5:     IAsyncResult asynResult = longTimeAction.BeginInvoke((result) => {
   6:         int ret = longTimeAction.EndInvoke(result);
   7:     }, null);
   8: }

當非同步作業完成後,上面代碼中用lambda運算式表示的一個回調方法就會執行,在這裡調用EndInvoke擷取耗時操作的結果。在這裡想想為什麼用lambda,如果不用lambda也不用匿名方法(不管你用啥,實際上就是形成一個閉包)你要怎麼做?留作您自己思考。

更新UI

上面四種非同步呼叫的方式:一種無聲無息,一去不複返。一種輪詢、一種等待,外加一個回調。實際上耗時操作的結果都讓代碼給“吃”了。一般情況下,我們處理完耗時操作總要有所表現吧,比如更新一下UI等等。那我們就來看看如何更新UI。

當你運行這個程式時,當耗時操作結束後,啪嚓一下,程式出異常了:

啊?為什麼啊,為什麼就不行啊。還不能從不是建立這個控制項的線程中訪問這個控制項。那怎麼辦?看來我們的非同步作業還得改進改進啊。

http://www.cnblogs.com/yuyijq/archive/2009/11/16/1603621.html

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.