C# 学习笔记(8) 控件的跨线程访问

本文参考博客
C#多线程 https://www.cnblogs.com/dotnet261010/p/6159984.html
C# 线程与进程 https://www.cnblogs.com/craft0625/p/7496682.html
C# 跨线程调用控件https://www.cnblogs.com/TankXiao/p/3348292.html
对于c#中的线程和进程,这两篇文章讲的相当到位了,本文只是为了学习做的摘要。

线程

  • 线程是什么?

线程(Thread)是进程中的基本执行单元,一个进程可以包含若干个线程,在.NET应用程序中,调用Main()方法时系统就会自动创建一个主线程。

  • 为什么要多线程?

在知道为什么要多线程前,先要知道什么是多线程?假设现在CPU有A、B、C、D、E五个任务,单线程就是A任务执行完毕接着执行B任务,B任务执行完毕接着执行C…就是一个一个的执行。多线程就是将时间分成时间片,假设一个时间片1ms,CPU在执行任务时,第一个1ms执行A任务,当第二个1ms来临时,CPU保存A任务的工作环境,然后去执行B任务…通过时间片任务ABCDE轮流执行,由于CPU很快,时间片时间很短,给我们一种ABCDE五个任务同时在运行的假象,这个就是多线程。

  1. 目前电脑都是多核多CPU的,在同一个时间片内,每一个CPU都可以运行一个线程,多线程可以提高CPU效率。
  2. 在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了程序的效率。
    简单来说,多线程主要是为了提高效率的,但是多线程需要注意 线程之间对共享资源的访问会相互影响,必须解决共享资源的竞争冒险问题。

单线程窗体卡死

创建一个窗体,在窗体上拖两个控件,textbox和按钮

将textbox滚动条属性打开

双击按钮,添加一个按钮单击事件,当按钮单击,textbox中打印10000一行数据。

 private void btnPrint_Click(object sender, EventArgs e){for(int i = 0; i < 10000; i++){txbLog.AppendText("这是第" +  i + "行\r\n");}}

发现当按钮按下后,textbox中有数据不断显示上去,但是这时无法操作窗体,窗体的移动,关闭等都无法操作,也就是俗称的窗体卡死。其实这就是单线程的弊端,当按键单击事件没有执行完毕,就不去响应其他的操作。

创建多线程

void PrintfLog()
{for (int i = 0; i < 10000; i++){txbLog.AppendText("这是第" + i + "行\r\n");}
}private void btnPrint_Click(object sender, EventArgs e)
{//创建线程Thread printThread = new Thread(PrintfLog);//告诉系统,这个线程准备好了,可以开始执行了,至于什么时候执行,看系统安排printThread.Start();
}

多线程创建十分简单,只需要创建和标记为开始状态即可。但是上面的代码如果直接仿真,会发现系统会抛出异常

根据异常提示,我们可以知道 txbLog控件是主线程创建的,不允许在其他线程直接调用它(在.NET上执行的是托管代码,C#强制要求这些代码必须是线程安全的,即不允许跨线程访问Windows窗体的控件。)

控件的跨线程访问

  1. 在窗体的加载事件中,将C#内置控件(Control)类的CheckForIllegalCrossThreadCalls属性设置为false,屏蔽掉C#编译器对跨线程调用的检查。

实际的软件开发中,做如此设置是不安全的(不符合.NET的安全规范)

 public partial class Form1 : Form{public Form1(){InitializeComponent();Control.CheckForIllegalCrossThreadCalls = false;}void PrintfLog(){for (int i = 0; i < 10000; i++){txbLog.AppendText("这是第" + i + "行\r\n");}}private void btnPrint_Click(object sender, EventArgs e){//创建线程Thread printThread = new Thread(PrintfLog);//告诉系统,这个线程准备好了,可以开始执行了,至于什么时候执行,看系统安排printThread.Start();}}
  1. 使用delegate和invoke来从其他线程中调用控件
public partial class Form1 : Form
{public Form1(){InitializeComponent();}//声明委托类delegate void txbLogPrintDelegate(string str);void PrintfLog(){for (int i = 0; i < 10000; i++){//通过txbLog的InVoke 告诉创建txbLog的线程,需要操作txbLog控件txbLog.Invoke((txbLogPrintDelegate)TxbLogAppendText, "这是第" + i + "行\r\n");}}void TxbLogAppendText(string str){//创建txbLog的线程也就是主线程 操作txbLog控件this.txbLog.AppendText(str);}private void btnPrint_Click(object sender, EventArgs e){//创建线程Thread printThread = new Thread(PrintfLog);//告诉系统,这个线程准备好了,可以开始执行了,至于什么时候执行,看系统安排printThread.Start();}
}

使用delegate和invoke来从其他线程中调用控件本质上还是通过主线程来操作控件,但是窗体并没有像单线程那样直接卡死,为什么? 对比在主线程里面操作控件和其他线程通过回调操作控件消耗时间,发现其他线程通过回调操作控件花费的时间远远大于主线程直接操作控件,猜测其他线程通过回调操作控件时,会发一个通知告诉主线程,主线处理完后就去处理其他UI事件了,表现也就是窗体没有卡死。 换了一台电脑卡死了,操作UI界面最终都要回到主线程,因此在频繁操作界面时,UI卡死嗯 挺正常的,毕竟你不能让他一边不停显示,一边又要移动,优化的话只能一边不全速的显示,另一半才能进行其他操作。

    public partial class Form1 : Form{public Form1(){InitializeComponent();}//声明委托类delegate void txbLogPrintDelegate(string str);void PrintfLog(){Stopwatch stopwatch = new Stopwatch();stopwatch.Start();for (int i = 0; i < 10000; i++){//通过txbLog的InVoke 告诉创建txbLog的线程,需要操作txbLog控件txbLog.Invoke((txbLogPrintDelegate)TxbLogAppendText, ":这是第" + i + "行\r\n");}stopwatch.Stop();txbLog.Invoke((txbLogPrintDelegate)TxbLogAppendText, stopwatch.ElapsedMilliseconds + "\r\n");}void TxbLogAppendText(string str){//创建txbLog的线程也就是主线程 操作txbLog控件this.txbLog.AppendText(Thread.CurrentThread.ManagedThreadId.ToString() + str);}private void btnPrint_Click(object sender, EventArgs e){txbLog.AppendText("主线程ID:" + Thread.CurrentThread.ManagedThreadId.ToString()+ "\r\n");Stopwatch stopwatch = new Stopwatch();stopwatch.Start();for (int i = 0; i < 10000; i++){//通过txbLog的InVoke 告诉创建txbLog的线程,需要操作txbLog控件txbLog.AppendText(Thread.CurrentThread.ManagedThreadId.ToString() + ":这是第" + i + "行\r\n");}stopwatch.Stop();txbLog.AppendText(stopwatch.ElapsedMilliseconds + "\r\n");//创建线程Thread printThread = new Thread(PrintfLog);//告诉系统,这个线程准备好了,可以开始执行了,至于什么时候执行,看系统安排printThread.Start();}}
  1. 使用delegate和BeginInvoke来从其他线程中控制控件

这个和方法二类似,只不过将 txbLog.Invoke 换成 txbLog.BeginInvoke 区别就是 Invoke方法是同步的, 它会等待主线程完成,BeginInvoke方法是异步的, 它会另起一个线程去完成工作线程 它会给主线程发生一个消息,等主线程空闲时再去执行

用上面代码 实际测试发现 txbLog.BeginInvoke也会导致界面卡死,不过想想也知道,操作UI界面最终都要回到主线程,因此在频繁操作界面时,UI卡死嗯 挺正常的,毕竟你不能让他一边不停显示,一边又要移动,优化的话只能一边不全速的显示,另一半才能进行其他操作。后面在别的电脑上测试,发现用 Invoke 和 BeginInvoke都不能避免UI卡死,最终给线程加了一个休眠,不让UI全速显示,嗯正常了,笔者初学乍到,如果大佬有什么更好的方法,敬请告知。

void PrintfLog()
{Stopwatch stopwatch = new Stopwatch();stopwatch.Start();for (int i = 0; i < 1000; i++){//通过txbLog的InVoke 告诉创建txbLog的线程,需要操作txbLog控件txbLog.BeginInvoke((txbLogPrintDelegate)TxbLogAppendText, ":这是第" + i + "行\r\n");Thread.Sleep(1);}stopwatch.Stop();txbLog.Invoke((txbLogPrintDelegate)TxbLogAppendText, stopwatch.ElapsedMilliseconds + "\r\n");
}
  • 这样的写法有一个烦人的地方:对不同的控件写法不同。对于TextBox,要TextBoxObject.Invoke,对于Label,又要LabelObject.Invoke。有没有统一一点的写法呢?

主窗口类本身也有Invoke方法。如果你不想对不同的控件写法不一样,可以全部用this.Invoke:

推荐使用主窗口类本身的Invoke方法再加上Lamda表达式进行控件跨线程访问

void PrintfLog()
{Stopwatch stopwatch = new Stopwatch();stopwatch.Start();for (int i = 0; i < 10000; i++){//通过txbLog的InVoke 告诉创建txbLog的线程,需要操作txbLog控件//this.Invoke((txbLogPrintDelegate)TxbLogAppendText, ":这是第" + i + "行\r\n");this.Invoke(new Action<string>((string str) => { txbLog.AppendText(Thread.CurrentThread.ManagedThreadId.ToString() + str); }), ":这是第" + i + "行\r\n");}stopwatch.Stop();txbLog.Invoke((txbLogPrintDelegate)TxbLogAppendText, stopwatch.ElapsedMilliseconds + "\r\n");
}
  1. 使用BackgroundWorker组件

正常情况下,有一个比较耗时的操作比如说算法很复杂、要写入数据库等IO操作,CPU不想在这里傻等(只有主线程的话,傻等会导致界面假死)从而创建一个线程来做这个耗时操作,耗时操作结束后操作UI界面给用户一个反馈,上面介绍的方法2、3都是控件的跨线程访问方法。下面介绍一个官方为解决这种问题提供的一种法方。

仔细研究会发现这种方法是基于方法2实现的,只不过用了官方的壳,其本质是在后台线程里干一些耗时操作,耗时操作干完后,通过回调在主线程里面完成其他操作,自己完全可以通过方法2实现

public partial class Form1 : Form
{public Form1(){InitializeComponent();}void PrintfLog(object sender, DoWorkEventArgs e){// 这里系统会自动生成一个后台线程// 可以在这里做一些费时的,复杂的操作StringBuilder stringBuilder = new StringBuilder(1024 * 1024);for (int i = 0; i < 10000; i++){//生成log信息stringBuilder.Append(Thread.CurrentThread.ManagedThreadId.ToString() + ":这是第" + i + "行\r\n");}//做完耗时操作后将结果通过 e.Result 传递出去e.Result = stringBuilder.ToString();}void TxbLogAppendText(object sender, RunWorkerCompletedEventArgs e){//这时后台线程已经完成,并返回了主线程,所以可以直接使用UI控件了 this.txbLog.AppendText(e.Result.ToString());}private void btnPrint_Click(object sender, EventArgs e){txbLog.AppendText("主线程ID:" + Thread.CurrentThread.ManagedThreadId.ToString() + "\r\n");using (BackgroundWorker bw = new BackgroundWorker()){//后台线程需要执行的委托bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(TxbLogAppendText);//后台线程结束后 会调用该委托bw.DoWork += new DoWorkEventHandler(PrintfLog);//如果线程需要参数,可以传入参数  DoWorkEventArgs e.Argument调用参数bw.RunWorkerAsync();}}
}

UI假死总结

上面2、3举的例子说明了一个问题,UI控件都在主线程 创建的情况下,频繁操作UI导致界面卡死是正常操作,可以通过给频繁操作的控件加上一定休眠时间在一定程度上解决

前后台线程

前台线程:只有所有的前台线程都结束,应用程序才能结束。默认情况下创建的线程都是前台线程
后台线程:只要所有的前台线程结束,后台线程自动结束。通过Thread.IsBackground设置后台线程。必须在调用Start方法之前设置线程的类型,否则一旦线程运行,将无法改变其类型。

  • 通过BeginXXX方法运行的线程都是后台线程。

  • 我们在使用上面delegate和invoke来从其他线程中调用控件 的代码进行打印时,再尚未打印结束的时候点击窗体上的叉号关掉窗体,会发现系统抛出异常,告诉我们目标不存在,为什么目标不存在?

创建的线程默认是前台线程,关闭窗体后前台线程并不会结束,因此还在调用控件txbLog,但是txbLog控件是主线程的,主线程已经关闭了,控件自然也就销毁了,因此在创建线程时,我们可以根据线程的重要程度,将线程设置为前台或者后台,一般情况下设置为后台线程即可(主线程结束后,后台线程就会自动结束);重要的线程,设置为前台线程,如果需要可以在窗体FormClosing事件中处理

//创建线程
Thread printThread = new Thread(PrintfLog);
//设置为后台线程
printThread.IsBackground = true;
//告诉系统,这个线程准备好了,可以开始执行了,至于什么时候执行,看系统安排
printThread.Start();

C# 学习笔记(8) 控件的跨线程访问相关推荐

  1. C# 委托 / 跨线程访问UI / 线程间操作无效: 从不是创建控件“Form1”的线程访问它...

    C# 委托 / 跨线程访问UI /  线程间操作无效: 从不是创建控件"Form1"的线程访问它 网上的代码都比较复杂,还是这个简单 见代码, 简易解决办法: 主窗体代码 usin ...

  2. WinForm中新开一个线程操作窗体上的控件(跨线程操作控件)GOOD

    http://www.cnblogs.com/joey0210/p/3450379.html 最近在做一个winform的小软件(抢票的...).登录窗体要从远程web页面获取一些数据,为了不阻塞登录 ...

  3. C#线程间操作无效: 从不是创建控件 XX 的线程访问它

    转自:http://www.arasplm.net/index.php/zh/community/myblog/c-xx-.html 前些天做的要使用到线程的项目,现在和大家分享一下感受! 以下面小列 ...

  4. C#线程间操作无效:从不是创建控件“textbox1”的线程访问它

    在C#的多线程访问中,在线程间的相互访问时因为线程安全问题有访问限制,在创建一般线程时,对于界面元素访问时这样的问题比较常见. 比如,创建一个form1,上面放置一个textbox控件,创建一个线程去 ...

  5. System.InvalidOperationException:“线程间操作无效: 从不是创建控件“txtPortName02”的线程访问它。”...

    "System.InvalidOperationException"类型的未经处理的异常在 System.Windows.Forms.dll 中发生 其他信息: 线程间操作无效: ...

  6. 【问题解决】线程间操作无效:从不是创建控件“textBox1”的线程访问它

    [问题解决]线程间操作无效:从不是创建控件"textBox1"的线程访问它 参考文章: (1)[问题解决]线程间操作无效:从不是创建控件"textBox1"的线 ...

  7. 其他信息: 线程间操作无效: 从不是创建控件“控件名”的线程访问它。

    在多线程程序中,新创建的线程不能访问UI线程创建的窗口控件,如果需要访问窗口中的控件,有以下解决办法 1.可以在窗口构造函数中将CheckForIllegalCrossThreadCalls设置为 f ...

  8. VC学习笔记 -单选按钮控件(Ridio Button)的使用

    在VC++编程过程中,查资料是一个苦差事,案边放了一摞书左翻右翻好是烦人.一赌气就把一些常用的小技巧自己总结了一下,虽费了些功夫,但对以后编程很有好处.现拿出来与大家共享,以后积累多了,作一个CHM电 ...

  9. Asp.net控件开发学习笔记(三)-控件开发基础

    封装      在asp.net中,控件被分为两类.用户控件和自定义服务器控件.前者就是我们经常用来将一些可复用的内容封装成的.ascx文件.这里主要研究后者. 创建自定义服务器控件      创建自 ...

最新文章

  1. MATLAB 与 Excel 接口
  2. centos7和scientific linux7里面调出中文输入法
  3. cdn.cache.php,CDN缓存不命中诊断 - 在线工具
  4. java点击按钮弹出警告_GUI求教~~~我想点击按钮确定后,弹出一个提示框输入有误!,,…...
  5. python中常量池和堆_JVM详解之:运行时常量池
  6. python模拟登录百度贴吧_Python百度贴吧多线程自动登录签到/自动打码
  7. 年货节买什么东西好?2022新年好物推荐
  8. 微慕小程序专业版V3.6.6发布
  9. Java随笔记 - 实现一个自定义的BitMap
  10. 关于Zbar和ZXing这两个无比强大的二维码和条形码识别工具
  11. 离散数学蕴含等值式前件为假命题为真的理解
  12. MySQL表锁了如何解锁
  13. google账户配置foxmail和使用foxmail
  14. 解码保存全部BMP图像
  15. QGC地面站对PX4无人机速度进行限制
  16. 《Linux运维总结:内网服务器通过代理访问外网服务器(方法一)》
  17. 云计算技术及应用选择题
  18. 在ubuntu中进行简单截屏、专业截屏、自定义截屏操作
  19. TextViewVertical实现文字并列竖排 如古诗,蒙古语等
  20. linux window连接软件,教你从Windows以图形方式远程连接Linux

热门文章

  1. ios开发入门篇(四):UIWebView结合UISearchBar的简单用法
  2. Android接口和框架学习
  3. android 4G产品4G网络问题记录
  4. windows10 IOT +Azure会议概要总结
  5. CTO俱乐部下午茶:技术团队管理中的那些事儿
  6. spring集成kafka
  7. shell 常用命令语句
  8. 折衷的方式实现php与ruby共享session实现单点登录
  9. poj1740 A New Stone Game
  10. SQLServer导入Excel截断数据的解决办法