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

    本文为复习……

  • 相关阅读:
    狄慧201771010104《面向对象程序设计(java)》第十六周学习总结
    狄慧201771010104《面向对象程序设计(java)》第十五周学习总结
    狄慧201771010104《面向对象程序设计(java)》第十四周学习总结
    201771030122-王瑞梅 实验二 个人项目—《西北师范大学学生疫情上报系统》项目报告
    201771030122-王瑞梅 实验一 软件工程准备—<初读《构建之法--现代软件工程》>
    软件工程学习总结
    团队项目在GitHub合作开发管理流程
    2020 软件工程—— 中期获“衣”有感
    201771010131-王之泰 实验四 软件项目案例分析
    数据库连接和导出excal
  • 原文地址:https://www.cnblogs.com/guogangj/p/2870590.html
Copyright © 2020-2023  润新知