[复习].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。

本文为复习……

posted @ 2013-01-22 21:28  guogangj  阅读(7907)  评论(3编辑  收藏  举报