WinForm二三事(二)

作者: 橫刀天笑  來源: 博客園  發布時間: 2009-11-16 09:45  閱讀: 2974 次  推薦: 1   原文鏈接   [收藏]  

監視消息循環

上一篇文章中,我們討論了消息循環是響應用戶輸入的根本,還提到了在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”,這樣會打開線程的窗口,在這里我們可以看到類似下圖的圖片:
image

有箭頭指示的是正在執行的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。

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

callcontrolfromotherthread

啊?為什么啊,為什么就不行啊。還不能從不是創建這個控件的線程中訪問這個控件。那怎么辦?看來我們的異步操作還得改進改進啊。

(未完待續)

1
0
 
標簽:WinForm
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()