C# Winform的多线程
首更
main函数所在的线程为主线程,所谓的UI线程、界面线程指的就是这个线程。在这个线程里,通过Application.Run()实现了一个死循环,使得窗体能够一直显示1。这有点类似stm32的程序结构,同样都是main函数中通过死循环来实现某个功能。
main函数中定义了一个窗体变量MainForm,MainForm中有两个控件(成员变量),分别为按键变量btn,文本框变量txtDisp,还有两个方法(成员函数),分别为btn_Click()和txtDisp_TextChanged()。其中btn_Click()方法订阅了按键变量btn中的Click事件,触发条件是鼠标单击按钮;txtDisp_TextChanged()方法订阅了文本框变量txtDisp的TextChanged事件,触发条件是文本框有键盘输入。
我理解的,Winform中的事件的响应是基于中断机制的,跟stm32相同。主线程在不断地循环,当触发条件产生时,主线程停止当前的工作,执行订阅事件的方法,然后再继续之前的工作。为此我写了下面的测试代码,在调试模式下观察了threadIdForm,threadIdButton和threadIdTextbox的值,均为1,说明窗体的初始化、按键的响应和文本变化的响应都在一个线程里。基于此,我在响应按键的方法中用线程休眠模拟了一个耗时8秒的操作,然后运行窗体,点击按键,紧接着在文本框中输入一串字符串。但是输入的字符串并没有马上显示,而是等了大概8秒之后才显示出来。针对这个现象,我得出了三个结论:
1、从中断的角度看,事件的响应不允许嵌套,只有响应完一个事件才能响应下一个事件,无法在响应事件的过程中响应另外一个事件。事件之间的优先级只有时间顺序,先到先响应。
2、在文本框输入时,应该有一个buffer用来存储输入的内容,然后程序再把buffer中的内容取出来进行显示。这个存储是通过硬件来实现的,因为该程序有且仅有一条线程,就是主线程,而此时主线程正在响应按键,所以不可能是软件实现的。
3、主线程不应该执行太耗时的操作,不然的话会让其他事件得不到及时的响应。
针对第三点,解决方法就是将运算等耗时又繁琐的工作放在子线程中完成,让主线程只进行UI更新的工作。这就是多线程的意义。但是会有一个问题,子线程中如果需要对控件进行更改(比如给文本框添加内容)怎么办?这就是跨线程操作控件的问题,可能出现子线程和主线程相互冲突的情况。解决方法就是先将更改控件的代码封装成一个方法,然后把这个方法绑定到一个委托上,通过invoke()方法或者begininvoke()方法,子线程将这个委托提交给主线程,由主线程调用这个方法进行控件的更新1,2,3,4。这个过程可以理解为子线程将更改控件的函数的地址传递给了主线程,由主线程来调用函数。在这种情况下,主线程还是会被占用。这跟我之前理解的多线程不太一样,我以为多线程就是各个线程相互独立,将一个任务分解成多个子任务,然后同时进行。现在看来我的理解是片面的,线程之间可以进行通信,那么就可能会出现子线程1让子线程2执行某个任务,导致子线程2被占用的情况。这并没有掩盖多线程的好处,在Winform中,多线程确实让主线程(UI线程)减负了好多5。
关于invoke()和begininvoke()还有一番学问,合起来有四种,分别是控件的invoke()、控件的begininvoke()、委托的invoke()和委托的begininvoke()。我看了许多博文6,7,8,但仍然云里雾里。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;
namespace DTU_Tool
{
public partial class MainForm : Form
{
string threadIdForm;
string threadIdButton;
string threadIdTextBox;
public MainForm()
{
InitializeComponent();
threadIdForm = Thread.CurrentThread.GetHashCode().ToString(); // threadIdForm = 1
}
private void btn_Click(object sender, EventArgs e)
{
threadIdButton = Thread.CurrentThread.GetHashCode().ToString(); // threadIdButton = 1
Thread.Sleep(8000); // 模拟一个耗时8秒的操作
}
private void txtDisp_TextChanged(object sender, EventArgs e)
{
threadIdTextBox = Thread.CurrentThread.GetHashCode().ToString(); // threadIdTextBox = 1
}
}
}
其他:
visual studio的多线程程序调试方法
二更(2020/7/15)
如下是一份跨线程操作的实例。
过程就是打开串口,点击发送按钮,向下位机发送一个字符s,下位机接收到字符后将字符发送回上位机。此时ReceiveData()方法被调用,方法中创建一个新的TextBox。
在ReceiveData()的入口打个断点进行逐步调试,可以看到,当ReceiveData()方法被调用时,所在的线程ID为12056,这是一个子线程。if条件成立,执行第75行的Invoke()方法9,然后重新进入ReceiveData()方法,此时线程ID变为了28576,也就是Main Thread线程。继续按F10,if条件不成立,执行第78到80行的代码,创建了一个写着“test"的TextBox,函数退出。创建TextBox的过程始终都在Main Thread中进行。退出后,重新回到第75行的Invoke()方法处,此时线程ID又变回28576。继续按F10,执行return,然后ReceiveData()执行结束,窗体显示新创建的TextBox。
通过这个调试过程可以比较直观地理解“线程间操作无效:从不是创建控件‘xxx’的线程访问它”这个错误以及Invoke()方法的执行过程:子线程中调用了Invoke()方法,然后阻塞,主线程通过Invoke()方法传递过来的委托(事实上就是一个函数指针)执行绑定在其上的方法,执行结束后子线程才继续执行。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO.Ports;
using System.IO;
namespace WindowsFormsApp1
{
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
}
private void MainForm_Load(object sender, EventArgs e)
{
//System.Windows.Forms.Control.CheckForIllegalCrossThreadCalls = false;
cbBaudrate.Items.Add(115200);
cbBaudrate.SelectedItem = 115200;
cbDataBits.Items.Add(8);
cbDataBits.SelectedItem = 8;
cbStopBits.Items.Add(1);
cbStopBits.SelectedItem = 1;
cbParityBits.Items.Add("None");
cbParityBits.SelectedItem = "None";
string[] ports = SerialPort.GetPortNames();
if (ports != null)
cbCOM.Items.AddRange(ports);
}
private void btnOpen_Click(object sender, EventArgs e)
{
if (btnOpen.Text == "Open")
{
serialPort.PortName = cbCOM.Text.ToString();
serialPort.BaudRate = int.Parse(cbBaudrate.SelectedItem.ToString());
serialPort.DataBits = int.Parse(cbDataBits.SelectedItem.ToString());
serialPort.StopBits = (StopBits)Enum.Parse(typeof(StopBits), cbStopBits.SelectedItem.ToString());
serialPort.Parity = (Parity)Enum.Parse(typeof(Parity), cbParityBits.SelectedItem.ToString());
serialPort.Open();
btnOpen.Text = "Close";
serialPort.DataReceived += new SerialDataReceivedEventHandler(ReceiveData);
}
else
{
serialPort.Close();
btnOpen.Text = "Open";
}
}
private void btnSend_Click(object sender, EventArgs e)
{
serialPort.Write("s");
}
private void ReceiveData(object sender, EventArgs e)
{
AddTextBox();
}
private void AddTextBox()
{
if (this.InvokeRequired)
{
this.Invoke(new MethodInvoker(delegate { AddTextBox(); }));
return;
}
TextBox tb = new TextBox();
tb.Text = "test";
this.panel1.Controls.Add(tb);
}
}
}