本文参考博客
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五个任务同时在运行的假象,这个就是多线程。
创建一个窗体,在窗体上拖两个控件,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窗体的控件。)
实际的软件开发中,做如此设置是不安全的(不符合.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(); } }
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事件了,表现也就是窗体没有卡死。
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(); } }
这个和方法二类似,只不过将 txbLog.Invoke 换成 txbLog.BeginInvoke 区别就是 Invoke方法是同步的, 它会等待工作线程完成,BeginInvoke方法是异步的, 它会另起一个线程去完成工作线程
用上面代码 实际测试发现 txbLog.BeginInvoke也会导致界面卡死,不过想想也知道,操作UI界面最终都要回到主线程,因此在频繁操作界面时,UI卡死嗯 挺正常的,毕竟你不能让他一边不停显示,一边又要移动,优化的话只能一边不全速的显示,另一半才能进行其他操作。
前台线程:只有所有的前台线程都结束,应用程序才能结束。默认情况下创建的线程都是前台线程
后台线程:只要所有的前台线程结束,后台线程自动结束。通过Thread.IsBackground设置后台线程。必须在调用Start方法之前设置线程的类型,否则一旦线程运行,将无法改变其类型。
创建的线程默认是前台线程,关闭窗体后前台线程并不会结束,因此还在调用控件txbLog,但是txbLog控件是主线程的,主线程已经关闭了,控件自然也就销毁了,因此在创建线程时,我们可以根据线程的重要程度,将线程设置为前台或者后台,一般情况下设置为后台线程即可(主线程结束后,后台线程就会自动结束);重要的线程,设置为前台线程,如果需要可以在窗体FormClosing事件中处理
//创建线程 Thread printThread = new Thread(PrintfLog); //设置为后台线程 printThread.IsBackground = true; //告诉系统,这个线程准备好了,可以开始执行了,至于什么时候执行,看系统安排 printThread.Start();