你有没有遇到过这种情况:写了一个程序,点了一下按钮,整个界面就像PPT一样卡住了,拖不动、点不了,过一会儿才恢复。
刚开始学编程的时候,我也被这个问题折磨过很多次。后来才知道,这叫“同步阻塞”。今天我们就来聊聊这个问题——什么是同步,它为什么会卡❓️
第一个、我们且先来看一个生活场景
假设我们今天有三件事要做:
去楼下取快递(来回10分钟)
给电脑装个软件(5分钟)
回一封邮件(3分钟)
同步的做法是:做完一件,再做下一件。
你先下楼取快递,来回10分钟。回来后开始装软件,盯着进度条等5分钟。装完了,再花3分钟回邮件。
总共耗时:10 + 5 + 3 = 18分钟。
这18分钟里,你明明可以在等快递电梯的时候用手机回邮件,可以在软件下载的时候去取快递——但你没有,你选择了“做完一件再做下一件”。
同步程序就是这个逻辑:一个任务执行完,才会执行下一个
第二个、在代码里,“同步”长什么样?其实我们都特别熟悉
整个程序从上到下,一句一句执行。PickUpPackage() 没执行完,InstallSoftware()就永远不会开始。这就是同步执行——代码按顺序执行,一个做完再做下一个。
第三个、辣么同步的“坑”:卡顿从哪来❓️
上面的demo程序是我们是用控制台编写的,感觉也就还好。但如果是图形界面程序(WinForm / WPF / 移动App),问题就来了。
我们想象一下:你点了一个“下载文件”的按钮,程序开始下载。下载过程中,界面完全卡住——不能拖动窗口、不能点击其他按钮、甚至不能最小化。用户会以为程序啊❓️程序是不是卡了,都不动了呢。
辣么,问题来了。为什么会卡呢❓️
因为图形界面程序有一个“主线程”,专门负责处理界面交互(响应点击、刷新界面等)。如果你的主线程正在执行一个耗时的同步操作(比如下载文件、查询数据库、计算大量数据),它就没空去处理界面交互了。
主线程被“堵”住了,界面就动不了了。
💡 主线程 = 唯一一个服务生
可以把主线程想象成餐厅里唯一的服务生。
同步模式:服务生给客人点完菜,就站在后厨门口等着,等菜做好端上桌,再去服务下一桌客人。其他客人喊他,他也没空搭理。
这就是“卡顿”——服务生被一个任务“占住”了,没空做别的。
第四个、刚才那个demo,就是在模拟“控制台卡顿”
这三行Thread.Sleep,
分别模拟了取快递、装软件、回邮件的耗时过程。
Thread.Sleep的意思是:让当前线程停住,什么都不做,等指定的时间过去再继续。
所以,当程序执行到 Thread.Sleep(10000) 时,主线程就“卡”住了10秒——不能执行后面的代码,不能响应任何操作。在控制台里,你看到的是一片空白,光标停在那里不动。在图形界面里,这就是窗口变灰、拖不动、点不了的根本原因。
同步的代价,就藏在这几行Thread.Sleep 里。
任务越多、耗时越长,主线程被占用的时间就越久,用户等待的时间就越长。
第五个、同步编程的优与劣
任何技术都有两面性,同步也一样。
优点:
缺点:
阻塞主线程:界面卡死,用户体验差
资源浪费:等待的时候,CPU也在空转
扩展性差:如果要做多任务,同步的效率很低
六、说个你可能已经发现的事
你可能会觉得:“同步就是按顺序执行,一个做完再做下一个——这不就是我们最开始学的顺序结构吗?”
完全正确。
C#程序的三大基本结构是顺序、分支、循环。其中顺序结构的意思就是:代码从上往下,一句一句执行。同步,就是顺序结构在“耗时任务”这个场景下的叫法。
只不过以前我们学顺序结构的时候,每一行代码都执行得很快——定义一个变量、调用一个方法、打印一行输出,都是毫秒级的事情,你感觉不到“卡顿”。
但当某一行的执行时间变得很长——比如下载文件、查询数据库、读取一个大文件——它依然遵守顺序结构的规则:这行没跑完,后面的就得等着。
这个时候,我们就给它换了个名字:同步阻塞。
“同步”说的是执行方式(按顺序),“阻塞”说的是后果(后面的被挡住了,界面卡住了)。
所以你其实早就懂了同步的概念,只是不知道“这个东西有名字”而已。
写在最后
今天我们只讲了一件事:同步就是按顺序执行,一个做完再做下一个。
同步代码简单直接,但当它在主线程上执行耗时操作时,界面就会卡成PPT。你点一下按钮,界面就冻住了——这在图形界面里非常致命。
那怎么解决这个问题呢?答案是——异步。
怎么让主线程不被卡住?怎么在“等待”的时候让程序继续干活?下期我们讲 async 和 await。
——路人甲的代码小馆,我们下期见!