非UI线程更新UI界面的各种方法小结

来源:互联网 发布:暗黑破坏神3mac 编辑:程序博客网 时间:2024/04/29 13:29

我们知道只有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

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 孕早期肚子紧绷怎么办 怀孕2个月同房了怎么办 怀孕前三月同房了怎么办 人流前三天同房了怎么办 人流后三天同房了怎么办 吃避孕药后月经量少怎么办 排卵日同房不想怀孕怎么办 排卵日同房没有怀孕怎么办 想怀孕想生儿子怎么办 到期大姨妈不来怎么办 怕怀孕月经不来怎么办 宫颈环扎后宫缩怎么办 来例假刺激外阴高潮了怎么办 客人消费完说没钱怎么办? 孕13周血糖偏高怎么办 餐后血糖高14点怎么办 餐后血糖20多怎么办 空腹血糖6.6饭后9.2怎么办 键盘shift键失效了怎么办 灵异附身夫妻不合怎么办 鞋子买大了半码怎么办 鞋子买小了半码怎么办 黑布鞋鞋面白了怎么办 黑布鞋不黑了怎么办 黑布鞋退白了怎么办 老北京布鞋款式太少怎么办 老人输液抽搐后昏迷怎么办 头七家里有狗怎么办 股票退市股民的钱怎么办 美国股票退市股民怎么办 百度云字幕和视频不同步怎么办 百度云加载字幕有延迟怎么办 很难适应新环境怎么办 蜘蛛丝碰到嘴唇上起包有毒怎么办? 电瓶车在路上爆胎了怎么办 嘴被虫子咬肿了怎么办 高铁管家购票失败怎么办 高铁车厢空调冷怎么办 高铁票过了时间怎么办 网购火车票丢了怎么办 改签没有票了怎么办