this.Invoke 学习笔记

接上一篇《WinForm 串口助手实战笔记》。上一篇里接收数据那段代码,套了一层 this.Invoke,我当时说”照着抄就行”。这一篇专门把这件事讲透,让你不仅会用,还能明白它到底在干嘛。


一、先认识线程

程序运行起来后,其实是一根”线”在从上往下一行行执行代码。这根”线”,就叫线程。

一个程序里可以同时有好几根这样的线在跑,各干各的。我们这个串口助手里,主要有两根:

线程 谁在跑 负责什么
界面线程 程序一启动就在 专门管窗口、按钮、文本框这些界面
后台线程 串口收到数据时临时冒出来 专门盯着串口,数据一来就接住

你可以把界面线程理解成”专门管柜台的人”,把后台线程理解成”专门在仓库搬货的人”。


二、为什么串口收到数据,不能直接改界面

这里有一条 C# 的硬规矩,一定要记住:

界面上的控件,只有界面线程能碰。

串口收到数据,是”后台线程”接住的。它手里拿着数据,想去改文本框,但文本框是”界面线程”管的,它碰不了。后台线程硬要去碰,程序就直接报错、闪退。

所以问题就来了:数据在后台线程手里,控件却只能界面线程碰,这个矛盾怎么解?


三、this.Invoke 是怎么帮忙的

解决办法很简单,一句话:后台线程自己不碰,把”改控件”这件事转交给界面线程去做。

this.Invoke 就是干这个的。它的意思可以直白地读成:

请你(界面线程)帮我做下面这件事。

代码长这样:

1
2
3
4
5
6
7
8
9
private void sp_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
string msg = sp.ReadExisting();

this.Invoke(new Action(() =>
{
txtReceive.AppendText(msg);
}));
}

中间大括号里的 txtReceive.AppendText(msg) 才是真正”改文本框”的那一句。外面包的这一层 this.Invoke,就是”把这个活交给界面线程”。


image-20260829085437174


image-20260829085456256


image-20260829085509897


四、把代码拆开,一个词一个词看

我们把 this.Invoke(new Action(() => { … })) 拆成四小块,每一块是干嘛的:

this

就是”当前这个窗口”,也就是 Form1。因为 Invoke 这个方法,是窗口自带的,所以用 this 来调用。

Invoke

意思是”委托界面线程执行,并且等它执行完再继续”。后面第二节会细讲这个”等”字。

new Action(…)

Action 可以理解成”一个任务包裹”。括号里装的就是要执行的代码。new Action 就是”打一个包,把里面的代码装进去”。

() => { … }

这个箭头符号读作”就这样做”。箭头左边是参数(这里没有参数,所以是空括号),箭头右边是要执行的代码。整句可以读成:”没有参数,直接执行大括号里这段代码”。

连起来整句话的意思就是:

把”往文本框里追加 msg 这段代码”打成一个包,交给界面线程去执行,并且等它执行完。


五、同步还是异步:Invoke 和 BeginInvoke

上面说到,Invoke 会”等执行完再继续”。这个”等”,就是同步的意思。

其实还有一个兄弟,叫 BeginInvoke,它”不等”。这就是同步和异步的区别。

Invoke BeginInvoke
交完任务后 原地等,界面更新完才继续 不停留,马上继续跑
叫法 同步 异步
好处 顺序稳稳的,简单好理解 后台线程不会被界面拖住
代价 界面慢的话,后台线程会被拖住 大量数据时,顺序可能乱

用大白话比喻:

  • Invoke(同步):你去办事窗口交材料,然后站在原地等,工作人员办完了你才走。稳,但工作人员磨蹭你就得干等。
  • BeginInvoke(异步):你把材料往窗口一递,扭头就走,不等人办完。快,但材料多了,办理顺序可能跟你递交的顺序不一样。

代码对比:

1
2
3
4
5
6
7
8
9
10
11
// 同步:等界面更新完,才继续
this.Invoke(new Action(() =>
{
txtReceive.AppendText(msg);
}));

// 异步:不停留,说完就走
this.BeginInvoke(new Action(() =>
{
txtReceive.AppendText(msg);
}));

用法几乎一模一样,就差一个 Begin 前缀。


六、到底该用哪个

记住一个简单的判断标准:

  • 数据量不大、偶尔来几条:用 Invoke 就行。简单、稳、不容易出错,新手首选。
  • 数据量非常大、更新非常频繁(比如一秒钟几百上千条):考虑用 BeginInvoke。因为 Invoke 每次都要等界面更新完,界面如果更新得慢,接收线程就会被卡住,数据越积越多。

用 BeginInvoke 的好处,就是避免接收线程被界面更新阻塞,让收数据这件事能一直顺畅地跑下去。

不过 BeginInvoke 不是免费的:它不保证顺序,数据来得太猛时,界面可能来不及消化,甚至出现堆积。所以简单串口助手用 Invoke 完全没问题,等你以后真遇到海量数据的场景,再切换 BeginInvoke,同时配合分批处理等手段。

一句话总结:入门阶段认准 Invoke,数据量巨大时才考虑 BeginInvoke。


七、进阶:更严谨的写法(可以先跳过)

实际项目里,还有一种更严谨的写法,会先判断一下”我现在是不是界面线程”。如果不是,才去委托;如果本来就是界面线程,就直接改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void sp_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
string msg = sp.ReadExisting();
UpdateReceive(msg);
}

private void UpdateReceive(string msg)
{
if (this.InvokeRequired)
{
// 当前不是界面线程,委托给界面线程
this.Invoke(new Action<string>(UpdateReceive), msg);
}
else
{
// 已经是界面线程,直接改
txtReceive.AppendText(msg);
}
}

这里新出现一个 InvokeRequired,它的意思是”需不需要委托”。是,就委托;不是,就直接改。

这段你眼下先看个眼熟就行,能看懂最好,看不懂完全不耽误。等线程这块你彻底吃透了,再回头来看它,会觉得特别自然。


八、小结

这篇你只需要记住三句话:

  1. 界面控件只有界面线程能碰,串口数据是后台线程收到的,所以不能直接改。
  2. this.Invoke 的作用,就是把”改控件”这件事交给界面线程去做,并且等它做完(同步)。
  3. 数据量巨大、更新频繁时,换 BeginInvoke,让接收线程不被界面更新拖住(异步)。

有了这个底子,以后你在任何”后台收到东西、要显示到界面”的地方,都知道该怎么写了。