zoukankan      html  css  js  c++  java
  • 【基础】多线程更新窗体UI的若干方法

    前言

    在单线程中设置窗体某个控件的值很简单的事,只需要设置控件文本的值就可以了,但是有的业务场景很是复杂,界面上的控件也很多,这种情况下当数据量比较多的时候,在单线程中更新UI不可避免地会发生假死或卡顿现象,用户体验十分不爽,所以必须采用多线程来处理数据和UI。但是如果直接添加一个线程来更新控件信息,就会抛出错误,很显然微软并不希望我们这样做,因为UI控件不是线程安全的,如果随意地在任何线程中改变控件的值,会发生各种奇怪的问题,多个线程间会争夺资源,没有秩序地更改控件的值,显然这是我们不想看到的结果。当然,解决这个问题的方式有很多,在此列出几种常用的方法,做一下记录,温故而知新。

    不检测线程间冲突

     由于业务的变更,导致界面控件和数据有所增加,单线程情况下性能堪忧,这时就需要从单线程切换到多线程,我们写的代码可能是这样:

     1 private void btnSetOrg_Click(object sender, EventArgs e)
     2 {
     3     Thread t = new Thread(new ParameterizedThreadStart(SetOrgValue));
     4     //Thread t = new Thread(SetOrgValue); 效果同上
     5     t.Start("Organization");
     6 }
     7 
     8 void SetOrgValue(object obj)
     9 {
    10     this.Org.Text = obj.ToString();
    11 }

    上述代码创建一个新的线程,并在新的线程中为Org控件赋值,看起来没什么问题,但当我们运行的时候,会报一个错误:线程间操作无效: 从不是创建控件“Org”的线程访问它。就是说上述代码中的t不是创建TextBox的线程,所以不能访问Org控件的属性。默认只能由创建该控件的线程去访问该控件的数据,否则会导致读写不一致。操作系统中介绍过PV操作,对此有很好的解释。对此有一个极为简单的方法可以避免上述错误,那就是不检测线程间冲突,只需要在构造函数中添加一行代码即可:

     1 public UIForm()
     2 {
     3 
     4     InitializeComponent();
     5     Control.CheckForIllegalCrossThreadCalls = false; //不检测跨线程调用
     6 }
     7 
     8 private void btnSetOrg_Click(object sender, EventArgs e)
     9 {
    10     Thread t = new Thread(new ParameterizedThreadStart(SetOrgValue));
    11     //Thread t = new Thread(SetOrgValue); 效果同上
    12     t.Start("Organization");
    13 }
    14 
    15 
    16 void SetOrgValue(object obj)
    17 {
    18     this.Org.Text = obj.ToString();
    19 }

    这段代码显然是为了应付线程间操作无效的错误,关闭了跨线程访问UI的检测,允许多线程随意更改控件数据,但最后控件的属性是什么值,只有天知道,所以并不提倡使用该方法。

    常用的多线程访问UI控件方式

    • 利用Delegate调用
    • 利用BackgroundWorker
    • 利用SynchronizationContext上下文
    • 利用Dispatcher.BeginInvoke

    首先说说委托调用的方式更新UI控件的值。每个控件都有一个Bool类型的InvokeRequired属性,表示该控件是否存在创建该控件以外的线程要访问该控件,如果值为True说明存在这样的线程,那么就需要利用委托调用来更新控件的值。把第二部分的代码变更如下:

     1 delegate void SetValue(object obj);
     2 
     3 private void btnSetOrg_Click(object sender, EventArgs e)
     4 {
     5     Thread t = new Thread(new ParameterizedThreadStart(SetOrgValue));
     6     //Thread t = new Thread(SetOrgValue); 效果同上
     7     t.Start("Organization");
     8 }
     9 
    10 private void UpdateOrg(object obj)
    11 {
    12     
    13     if (Org.InvokeRequired)//如果是非创建该控件的线程访问该控件
    14     {
    15         SetValue set = SetOrgValue;
    16         Org.Invoke(set, obj);
    17     }
    18     else
    19     {
    20         Org.Text = obj.ToString();
    21     }
    22 }
    23 
    24 private void SetOrgValue(object obj)
    25 {
    26     this.Org.Text = obj.ToString();
    27 }

    采用委托的方式,首先要检查访问控件的线程是否是创建该控件的线程,如果不是的话,就采用委托的方式去调用设值的方法。如果从另一个线程调用控件的方法,那么必须使用控件的Invoke方法来将调用封送到适当的线程。网上有一个有趣的比喻,如果某人向你(Org控件)借钱(访问并修改),如果直接去你的钱包拿(设置Org的属性值)是很不安全的,万一直接抢走了呢?所以需要提前告诉你一声说我需要借钱(委托),然后自己拿出钱(线程安全)借给借钱的人。这样理解起来就比较容易接受了。

    第二种方式是利用BackgroundWorker来访问控件,其实质上也是创建了新线程,只不过BackgroundWorker做了一个封装,使我们用起来更方便一些。

     1 private void btnSetOrg_Click(object sender, EventArgs e)
     2 {
     3     using (BackgroundWorker bw = new BackgroundWorker())
     4     {
     5         bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(RunWorkerCompleted);
     6         bw.DoWork += new DoWorkEventHandler(DoWork);
     7         bw.RunWorkerAsync("Organization");
     8     }
     9 }
    10 
    11 void RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    12 {
    13     //这时后台线程已经完成,并返回了主线程,所以可以直接使用UI控件了
    14     this.textBox1.Text = e.Result.ToString();
    15     MessageBox.Show(Thread.CurrentThread.ManagedThreadId.ToString());//查看当前线程的ID
    16 }       
    17 
    18 void DoWork(object sender, DoWorkEventArgs e)
    19 {
    20     MessageBox.Show(Thread.CurrentThread.ManagedThreadId.ToString());//查看当前线程的ID
    21     e.Result = e.Argument;//这里只是简单的把参数当做结果返回,当然也可以在这里做复杂的处理后,再返回自己想要的结果(这里的操作是在另一个线程上完成的)
    22 }

    第三种方式就是利用SynchronizationContext上下文。这种方式相对于上面的几种方式不是很常用。使用方式如下:

     1 private void btnSet_Click(object sender, EventArgs e)
     2 {
     3     Thread t = new Thread(new ParameterizedThreadStart(Run));
     4     OrgParam _org = new OrgParam() { Context = SynchronizationContext.Current, Org = "Organization" };
     5     t.Start(_org);
     6 }
     7 void Run(object obj) 
     8 {
     9     OrgParam org = obj as OrgParam;
    10     org.Context.Post(SetTextValue, org.Org);
    11 }
    12 
    13 void SetTextValue(object obj) 
    14 {
    15     this.Org.Text = obj.ToString();
    16 }
    17 public class OrgParam 
    18 {
    19     public SynchronizationContext Context { set; get; }
    20     public object Org { set; get; }
    21 }

    最后一种就是利用Dispatcher.BeginInvoke来更新控件的值。这种方式相对来说比较方便,但是也不是很常用。

    1 private void btnSetOrg_Click(object sender, EventArgs e)
    2 {
    3    Thread t = new Thread(new ParameterizedThreadStart(SetOrgValue));
    4    t.Start("Orgnization");
    5 }
    6 private void SetOrgValue(object obj)
    7 {
    8     this.Dispatcher.BeginInvoke(() => { this.txt.Text = text.ToString(); }); 
    9 }

    总结

    以上就是多线程下更新控件值的几种方式,虽然方式不一样,但都是为了解决多线程访问控件出现的问题。其中不检测线程间冲突的方法和采用委托的方式只限于WinForm下使用;而最后一种Dispatcher.BeginInvoke只限于Silverlight下使用;而BackgroundWorkerSynchronizationContext这两种方式是适用于Winform/Silverlight的。以前在写WinForm的时候,只知道采用委托的方式和BackgroundWorker来解决跨线程访问控件的问题,现在回头看看这块的内容,又知道了几种解决问题的方式,也许这就是孔子老人家所说的“温故而知新”吧。马上就要过年了,提前祝大家新年快乐!

    作者:悠扬的牧笛

    博客地址:http://www.cnblogs.com/xhb-bky-blog/p/6262790.html

    声明:本博客原创文字只代表本人工作中在某一时间内总结的观点或结论,与本人所在单位没有直接利益关系。非商业,未授权贴子请以现状保留,转载时必须保留此段声明,且在文章页面明显位置给出原文连接。

  • 相关阅读:
    Node.js 回调函数
    算法二、
    一、Perfect Squares 完全平方数
    Never Go Away
    python 内置方法
    web框架详解之tornado 三 url和分页
    web框架详解之tornado 二 cookie
    前端各种插件
    AJAX请求时status返回状态明细表
    LR之-参数化
  • 原文地址:https://www.cnblogs.com/xhb-bky-blog/p/6262790.html
Copyright © 2011-2022 走看看