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();
    }
  • 相关阅读:
    DVWA 黑客攻防演练(十)反射型 XSS 攻击 Reflected Cross Site Scripting
    DVWA 黑客攻防演练(九) SQL 盲注 SQL Injection (Blind)
    DVWA 黑客攻防演练(八)SQL 注入 SQL Injection
    DVWA 黑客攻防演练(七)Weak Session IDs
    DVWA 黑客攻防演练(六)不安全的验证码 Insecure CAPTCHA
    DVWA 黑客攻防演练(五)文件上传漏洞 File Upload
    工作流表结构设计
    Visual Studio 2019尝鲜----新建空项目体验
    《使用CSLA 2019:CSLA .NET概述》原版和机译文档下载
    .NET快速开发平台的在线预览
  • 原文地址:https://www.cnblogs.com/Jeely/p/11712034.html
Copyright © 2011-2022 走看看