zoukankan      html  css  js  c++  java
  • 非UI线程更新UI界面的各种方法小结

    转载:https://www.cnblogs.com/xiashengwang/archive/2012/08/18/2645541.html

    我们知道只有UI线程才能更新UI界面,其他线程访问UI控件被认为是非法的。但是我们在进行异步操作时,经常需要将异步执行的进度报告给用户,让用户知道任务的进度,不至于让用户误认为程序“死掉了”,特别是对于Winform,WPF等客户端程序尤为重要。

      那么我们要探讨的就是如何让非UI的任务线程更新UI界面。下面对已知的几种实现方式做个总结。随着.Net版本的不断升级,实现方式还可能会增加。

    1)使用Control.Invoke或Control.BeginInvoke。

    .Net1.1时允许非UI的线程访问UI控件,.Net2.0开始不允许了。所以程序员首先要检测Control的InvokeRequired属性,如果为true,就说明是非UI线程访问了这个控件,于是就需要调用这两个方法之一,将操作UI的函数封装到UI线程上去执行。其中Invoke是阻塞的,BeginInvoke是异步的。

    复制代码
            private delegate void ProgressChangedHander(int percentage);
            private void UpdateUI(int percentage)
            {
                if (this.progressBar1.InvokeRequired)
                {
                    //非UI线程,再次封送该方法到UI线程
                    this.progressBar1.BeginInvoke(new ProgressChangedHander(UpdateUI), new object[] { percentage });
                }
                else
                {
                    //UI线程,进度更新
                    this.progressBar1.Value = percentage;
                }        
            }
    复制代码

    2)利用同步上下文调度器

    .Net4.0增加了一个线程操作的类Task。Task的Start方法或ContinueWith方法中可以指定一个任务调度器TaskScheduler,如果这个任务调度器是同步上下文调度器,那么在Task的方法中就可以访问UI控件。要得到一个同步上下文调度器,需要通过TaskScheduler的静态方法FromCurrentSynchronizationContext。

    复制代码
                //得到一个同步上下文调度器
                TaskScheduler syncSch = TaskScheduler.FromCurrentSynchronizationContext();
    
                Task<int> t = new Task<int>(() => Sum(100));
    
                //在Task的ContinueWith方法中,指定这个同步上下文调度器,我们更新了form的Text属性
                //去掉这个syncSch,你就会发现要出异常
                t.ContinueWith(task => Text = task.Result.ToString(), syncSch);
                t.Start();
    复制代码

    PS: 其实TaskScheduler内部是使用SynchronizationContext实现的。

    3)利用同步上下文SynchronizationContext

    这个类很重要,利用这个类可以大大简化我们的异步更新UI界面的代码。避免了和线程间的无尽纠缠。利用SynchronizationContext的Current可以得到当前线程的同步上下文。注意,如果你在非UI线程上调用,会得到null。所以我们需要在UI线程上首先得到它的一个引用。然后在任务线程里就可以用这个引用变量。利用它的Send或Post方法将我们的更新UI的函数封送到UI线程上执行。对于WinForm程序来说Current返回的是WindowsFormsSynchronizationContext,它是SynchronizationContext的一个子类。Send或Post方法内部其实还是使用的Control.Invoke或Control.BeginInvoke来实现的。看一下它的Send方法:

    复制代码
    public override void Send(SendOrPostCallback d, object state)
    {
        Thread destinationThread = this.DestinationThread;
        if (destinationThread == null || !destinationThread.IsAlive) throw new InvalidAsynchronousStateException(SR.GetString("ThreadNoLongerValid"));
        //这里就是用的control的invoke方法
        if (this.controlToSendTo != null) this.controlToSendTo.Invoke(d, new object[] { state });
    }
    复制代码

    注意:Send方法是阻塞的,Post方法是异步的。

    喜欢刨根问底的,比如我,又在想,Control的Invoke是如何实现线程间的封送的呢?我们来略微调查一下。

    复制代码
    public object Invoke(Delegate method, params object[] args)
    {
        using (new MultithreadSafeCallScope())
        {
            return this.FindMarshalingControl().MarshaledInvoke(this, method, args, true);
        }
    }
    复制代码

    Invokie里调用了MarshaledInvoke方法。一看Marshal就知道有封送的意思。为了不偏离主题,对MarshaledInvoke这个方法的代码保留主要的部分,有个印象就行,大家不用太较真,毕竟是Mircrosoft内部的代码,没太多的闲工夫来研究。

    复制代码
    private object MarshaledInvoke(Control caller, Delegate method, object[] args, bool synchronous)
    {
        int num;
        //…
        bool flag = false;
       //判断是不是UI线程调用的Invoke 
    if (SafeNativeMethods.GetWindowThreadProcessId(new HandleRef(this, this.Handle), out num) == SafeNativeMethods.GetCurrentThreadId() && synchronous) flag = true;
        ExecutionContext executionContext = null;
        //如果不是,获得UI线程的执行上下文
        if (!flag) executionContext = ExecutionContext.Capture();
        //利用这个UI线程的上下文,构造一个线程调用方法入口
        ThreadMethodEntry entry = new ThreadMethodEntry(caller, this, method, args, synchronous, executionContext);
        lock (this)
        {
            if (this.threadCallbackList == null) this.threadCallbackList = new Queue();
        }
        lock (this.threadCallbackList)
        {
            if (threadCallbackMessage == 0) threadCallbackMessage = SafeNativeMethods.RegisterWindowMessage(Application.WindowMessagesVersion + "_ThreadCallbackMessage");//注册一个消息
            this.threadCallbackList.Enqueue(entry);//将调用方法加入线程调用队列
        }
        if (flag)
            this.InvokeMarshaledCallbacks();//同步,马上执行
        else
            UnsafeNativeMethods.PostMessage(new HandleRef(this, this.Handle), threadCallbackMessage, IntPtr.Zero, IntPtr.Zero);//异步:发送消息,UI得到消息就会调用
        if (!synchronous) return entry;
        if (!entry.IsCompleted) this.WaitForWaitHandle(entry.AsyncWaitHandle);
        if (entry.exception != null) throw entry.exception;
        return entry.retVal;
    }
    复制代码

    上面的方法的内部实现较为复杂,勉强注释了几个地方,一家之言,不可全信。大意可能大家都明白了,对于BeginInvoke异步调用,它用了消息泵,UI线程可以提取到这个消息,并执行相应的函数。而对于同步的Invoke忍不住又查了点:

    ExecutionContext.Run(tme.executionContext, invokeMarshaledCallbackHelperDelegate, tme);

    这里的tme就是ThreadMethodEntry,说明ExecutionContext的静态方法Run是不是实现了线程的切换呢?不再继续调查了,我们只用记住,Control的Invoke和BeginInvoke可以实现到UI线程的切换就行了。

    说着说着就远离主题了,下面来看看SynchronizationContext的用法:

    复制代码
           private void SyncContextTest()
            {
                //UI线程的ISynchronizationContext取得
                SynchronizationContext syncContext = SynchronizationContext.Current;
    
                //新建一个模拟操作i
                ThreadPool.QueueUserWorkItem((o) =>
                    {
                        for (int i = 0; i < 100; i++)
                        {
                            //模拟耗时
                            Thread.Sleep(100);
                            //通知用户
                            syncContext.Post(new SendOrPostCallback(ProgressCallBack), i);
                        }
                    }
                  );               
            }
            private void ProgressCallBack(object percent)
            {
                //不再判定是不是UI线程
                this.progressBar1.Value = (int)percent;
            }
    复制代码

      但是上面的代码还是有点缺陷,就是Post的回调函数参数只能是object的,要强行转换成int。但我们可以像下面这样修改,为用户提供一个int型的接口。

    复制代码
            delegate void UserNotifyProcess(int percent);
            private void SyncContextTest()
            {
                // UI线程的ISynchronizationContext取得
                SynchronizationContext syncContext = SynchronizationContext.Current;
    
                UserNotifyProcess userNotify = null;
                userNotify += new UserNotifyProcess(ProgressCallBack);
    
                //新建一个模拟操作i
                ThreadPool.QueueUserWorkItem((o) =>
                    {
                        for (int i = 0; i < 100; i++)
                        {
                            //模拟耗时
                            Thread.Sleep(100);
                            //通知用户
                            syncContext.Post((param) =>
                            {
                                //这里是关键了,只要到这里就说明是UI线程了
                                if (userNotify != null)
                                {
                                    userNotify((int)param);
                                }
                            },
                            i);
                        }
                    }
                  );
            }
    复制代码

    上面的代码只是一个测试代码,具体应该封装到一个类中,以提供事件的方式公开这个接口。

    关于SynchronizationContext的详细阐述,可以看看这篇很有价值的文章:

    http://www.codeproject.com/Articles/31971/Understanding-SynchronizationContext-Part-I

    http://www.codeproject.com/Articles/32113/Understanding-SynchronizationContext-Part-II

    http://www.codeproject.com/Articles/32119/Understanding-SynchronizationContext-Part-III

    4)使用Control.CheckForIllegalCrossThreadCalls

    Control类的静态成员CheckForIllegalCrossThreadCalls设置为false,UI线程创建的控件,允许被非UI线程访问,但是这种做法极度不推荐,这样会导致线程不安全。随着.Net版本的升级,这个属性很可能被禁用。不过暂时还可以使用。看代码:

    1
    2
    3
    4
    5
    public Form1()
    {
        CheckForIllegalCrossThreadCalls = false;//直接在构造函数加上这句就行了。
        InitializeComponent();
    }
  • 相关阅读:
    一百三十二:CMS系统之前端动态获取后台添加的轮播图
    一百三十一:CMS系统之轮播图上传图片功能
    一百三十:CMS系统之七牛js和python的SDK使用示例
    Python中文件编码的检测
    三目运算符
    三级菜单
    购物车程序
    计算机进制转换
    Python字典练习题
    VS Code常用快捷键总结
  • 原文地址:https://www.cnblogs.com/Jeely/p/11712034.html
Copyright © 2011-2022 走看看