前言
写代码就是这样,不是天天写的代码,很长一段时间不用,下次再用大概率是已经忘记了,不知道怎么处理。
虽然可以从搜索引擎中找到答案,但是肯定没有自己整理的看着舒服,所以后面会把 C/S 开发时遇到的一些小坑整理以下。
多线程操作 UI 组件
今天写一个 HTTP 监听的小工具,其中需要输出一个日志窗,其中会由于日志类型的不同,调整 RichTextBox
的 SelectionColor
和 SelectionBackColor
以更新日志窗体的前景色或背景色。
当然可能涉及多线程中输出日志,所以更新组件内容自然也用了 Invoke
与 BeginInvoke
方法。
因为写日志时会将之前设置的前景色以及背景色取出来,等输出完成后将原本的前景色或背景色设置回去,所以调用方法以后就在想,这个过程需不需要加锁。
因为万一在输出 Error
日志时,将颜色设置成红色了,那另外一个线程需要写 Info
类型的日志,那前景色与背景色岂不是就乱掉了。
然后就简单写了个段,测试了一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| for (int i = 0; i < 20; i++) { int num = i; Task.Factory.StartNew(() => { richTextBox1.BeginInvoke(new Action(() => { richTextBox1.SelectionColor = Color.Red; richTextBox1.AppendText($"{DateTime.Now:HH:mm:ss.fff} {Thread.CurrentThread.ManagedThreadId:00000} 任务一开始执行 {num}\r\n"); Thread.Sleep(10); richTextBox1.SelectionColor = Color.Red; richTextBox1.AppendText($"{DateTime.Now:HH:mm:ss.fff} {Thread.CurrentThread.ManagedThreadId:00000} 任务一执行结束 {num}\r\n"); })); }); Task.Factory.StartNew(() => { richTextBox1.BeginInvoke(new Action(() => { richTextBox1.AppendText($"{DateTime.Now:HH:mm:ss.fff} {Thread.CurrentThread.ManagedThreadId:00000} Task two begains {num}\r\n"); Thread.Sleep(1); richTextBox1.AppendText($"{DateTime.Now:HH:mm:ss.fff} {Thread.CurrentThread.ManagedThreadId:00000} Task two is over {num}\r\n"); })); }); }
|
执行效果:

可以看到输出的线程 ID 全部是主线程的 ID:00001
,所以这时候才想起来,无论使用同步方法 Invoke
,还是异步方法 BeginInvoke
,都仅仅知识针对 UI 主线程外的其他线程,实际上调用以后的委托只有一个 UI 线程来负责执行。
否则怎么可能避免 线程间操作无效: 从不是创建控件的线程访问它。
,所以就是杞人忧天了。
WinForm
的控件基类型 Control
提供了 Invoke
方法与 BeginInvoke
,多线程中如果需要操作 UI 组件(赋值操作),可以使用这两个方法。
区别是 Invoke
是同步方法,当前线程会等待 UI 主线程将该委托执行完成,而 BeginInvoke
是异步的则不会等待 UI 主线程的操作。
传递的委托我常常使用以下几种写法,都没有问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| Task.Factory.StartNew(() => { Color color = richTextBox1.SelectionColor;
this.BeginInvoke((Action)delegate { richTextBox1.SelectionColor = color; });
this.Invoke(new EventHandler(delegate { richTextBox1.SelectionColor = color; }));
richTextBox1.BeginInvoke(new Action(() => { richTextBox1.SelectionColor = color; })); });
|
注意:我们使用任何组件来执行 Invoke
或 BeginInvoke
都是一样的,例如上面这个例子,无论是使用 this
指代的当前窗体,还是这个窗体的富文本框 richTextBox1
,最终目的和效果都是在 UI 主线程中执行代码。
WPF
同 WinForm
一样,WPF 中主线程维护的 UI 子线程也不能直接更新,但是不同的是 WPF 是通过 Dispatcher
处理,由 Dispatcher 来管理线程工作项队列。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| Task.Factory.StartNew(() => { Brush brush = richTextBox1.Background;
this.Dispatcher.BeginInvoke((Action)delegate { richTextBox1.Background = brush; });
this.Dispatcher.Invoke(new EventHandler(delegate { richTextBox1.Background = brush; }));
richTextBox1.Dispatcher.BeginInvoke(new Action(() => { richTextBox1.Background = brush; })); });
|
其继承关系可以参考我从网上找到的一幅图:

注:因为 Windows XP
支持 .NET Framewrok
的最后一个版本是 .NET Framewrok 4.0
,所以没有特别说明,我习惯上创建的 Windows Form
或 WPF
等客户端程序选择的框架都是 .NET Framewrok 4.0
。
参考: