[復習].net的Invoke


話說接觸.net一年有余,發覺身邊許多用.net的人都不知道“線程”這回事,他們寫的程序都是單線程的,從不考慮把一個耗時較多的操作放到一個工作線程中,所以一旦數據庫操作長時間沒反應,程序界面也就跟着卡死了……而線程對於有着多年Windows編程經驗的我來說,再熟悉不過。

一般來說,程序的界面處理是用一個線程(通常同時作為主線程),而工作線程則可能有好幾個,比較理想的情況下據說是跟CPU的個數(現在准確說是跟CPU的核心數)相同,工作線程負責一些比較耗時的處理,如量較大的IO讀寫操作,工作線程一般不會操作界面元素,如果需要操作,則是通過向界面線程發消息的方式,而不是直接控制界面元素。

我記得在Windows編程(C++)中,並沒有一個硬性規定說工作線程一定不能操作界面元素,但我們通常確實不會那么干,因為這樣的話實際操作起來會有一些不可預知的問題,如工作線程莫名其妙被卡死,界面失去響應或者不按預期刷新等,所以界面元素的處理(包括繪制和響應用戶操作)都由一個線程來做,工作線程還是老老實實“干活”去,別越俎代庖,至於如何把工作的進度“匯報”到界面上去,那就只能通過“打報告”,即發送消息,而且只能用PostMessage,不可用SendMessage,因為SendMessage會阻塞線程等待返回。這不是唯一的做法,但卻是最正統的做法。(SendMessage和PostMessage是Windows的兩個原生API函數,可用C/C++直接調用)

到了.net(無論是Winform還是WPF),微軟用了兩個方法對PostMessage進行了封裝,分別是Invoke和BeginInvoke,Invoke的行為類似於SendMessage(其實底層上還是用PostMessage來實現,只是調用完之后就直覺開始等待),而BeginInvoke的行為則類似PostMessage。先來看這么一個最簡單的例子:界面上有一個進度條progressBarExecuting,有一個按鈕buttonExecuteManually,點擊一下按鈕,進度條前進10,我們這么寫:

        public void SetProgress(int iProgress)
        {
            progressBarExecuting.Value += iProgress;
        }

        private void buttonExecuteManually_Click(object sender, RoutedEventArgs e)
        {
            SetProgress(10);
        }

BeginInvoke在單線程程序中的用法

上面的代碼沒有任何問題,但我現在假設SetProgress是個比較耗時的操作,我不希望我對Click事件的處理被卡在這個上面,我希望buttonExecuteManually_Click立即結束,不管SetProgress到底執行如何,這怎么辦?這時候雖然沒有涉及到多線程,但BeginInvoke就可以派上用場了。

        private delegate void SetProgressMethod(int iProgress);

        public void SetProgress(int iProgress)
        {
            Debug.WriteLine("[{0}]SetProgress是個耗時的動作", DateTime.Now.TimeOfDay.TotalSeconds);
            Thread.Sleep(5000);
            progressBarExecuting.Value += iProgress;
            Debug.WriteLine("[{0}]SetProgress結束", DateTime.Now.TimeOfDay.TotalSeconds);
        }

        private void buttonExecuteManually_Click(object sender, RoutedEventArgs e)
        {
            Dispatcher.BeginInvoke(new SetProgressMethod(SetProgress), 10);
            Debug.WriteLine("[{0}]buttonExecuteManually_Click結束", DateTime.Now.TimeOfDay.TotalSeconds);
        }

Debug輸出結果:

[86295.9374504]buttonExecuteManually_Click結束
[86295.9384504]SetProgress是個耗時的動作
[86300.9394504]SetProgress結束

從這可以看出,Click事件的處理無需等待SetProgress,它直接結束掉了,這個在某些場合特別有用,如在處理一些需要及時返回的鼠標事件的時候,UI編程做多了自然能夠體會到這點。

使用Timer更新界面

現在我換一種方式更新進度條,那就是使用Timer,點擊按鈕激活Timer,並讓每100ms,進度條前進2。

        private Timer m_timerTest;
        
        private void buttonExecuteByTimer_Click(object sender, RoutedEventArgs e)
        {
            if (m_timerTest != null)
            {
                m_timerTest.Dispose();
            }
            progressBarExecuting.Value = 0;
            m_timerTest = new Timer(TestTimerCallback, null, 0, 100);
        }
        
        public void SetProgress(int iProgress)
        {
            progressBarExecuting.Value += iProgress;
        }

        public void TestTimerCallback(Object state)
        {
            SetProgress(2);
        }

運行,出錯了:

很顯然,Timer並不屬於界面線程,如果直接在Timer的線程中處理界面元素的顯示,就會出錯。另外這是跟標准的Windows編程很不一樣的地方,標准的Windows編程,Timer並不是一個線程,而是向系統注冊一個Timer之后,由系統定時往線程消息隊列中插入WM_TIMER消息來實現的,在.net中改作獨立線程的原因我想是因為需要更少的界面干預吧,純猜測。

那么正確的做法應該是怎樣呢?很簡單,稍微改一點點代碼:

        private delegate void SetProgressMethod(int iProgress);

        public void TestTimerCallback(Object state)
        {
            Dispatcher.BeginInvoke(new SetProgressMethod(SetProgress), 2);
        }

這樣就OK了,這是WPF的情況,如果使用的是WinForm,那就調用對應Control的BeginInvoke。那,這里用Invoke行不行?當然行,但通常我們會用BeginInvoke,因為如前面所說,Invoke是阻塞的,其作用沒BeginInvoke大。

使用一個獨立線程更新界面

其實跟同Timer沒什么差別,Timer是線程,線程更是線程,對不?

        private Thread m_threadTest;
        private AutoResetEvent m_eventStop = new AutoResetEvent(false); 
        private delegate void SetProgressMethod(int iProgress);
        
        public void TestThread()
        {
            do
            {
                if (m_eventStop.WaitOne(100))
                    return;
                Dispatcher.BeginInvoke(new SetProgressMethod(SetProgress), 2);
            } while (true);
        }

        private void buttonExecuteByThread_Click(object sender, RoutedEventArgs e)
        {
            progressBarExecuting.Value = 0;
            m_threadTest = new Thread(TestThread);
            m_threadTest.Start();
        }
        
        private void Window_Closed(object sender, EventArgs e)
        {
            m_eventStop.Set();
        }

和Timer不同之處是這里用了一個AutoResetEvent,其初始是無信號的,在窗口關閉時候將其變為有信號,這樣工作線程會收到這個信號,並“優雅地”return,而不是Terminate。

其它情況

有時候你還會不經意地使用了線程,但並非顯式地創建Thread,比如有一次我寫了一個監視某個文件夾的程序,當此文件夾的文件發生變化(增加,刪除,修改等)時候,我的回調函數就被調用,底層上來看,這也是開一個線程來做的,所以我的回調函數不能直接操作界面元素,必須用BeginInvoke或者Invoke。

本文為復習……


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM