你真的了解多线程吗?
如果问你“为什么多线程可以提高程序运行效率?”,想必你会说“计算机并行执行任务啊,当然效率高!” 这显然不是一个内行该给出的答案。要知道,一个 CPU 在任何时间点上只能干一件事情啊,如果执行了指令 A,就不能执行指令 B 了,何来并行呢?想要真正理解多线程,必须得先弄清 linux 中的 I/O 机制。
什么是 Linux I/O ?
一般指文件读取/写入、网卡读取/写入。不管是文件 I/O,还是网卡 I/O,其准备数据速度都远远低于 CPU 处理数据的速度 。磁盘读数据,毫秒级别,最慢。其次是网卡,微秒。再就是内存,纳秒,最快的是缓存。
下图显示了一个 Linux 进程(只有一个主线程)的执行过程:
- 用户进程向 CPU 发起 read 请求,CPU 向 DMA 发起 I/O 请求,DMA 再向磁盘发起 I/O 请求。这里的 DMA 全称为 Direct Memory Access,你可以简单理解为 CPU 内存和磁盘之间的代理。
- 磁盘向 CPU 发起 I/O 中断,CPU 拷贝数据,先是内核态 buffer,再到用户态 buffer。
- 用户进程从内核态切回用户态,阻塞状态结束,程序继续执行,直到结束。
上述流程中,用户进程在拿到磁盘数据之前,只能原地等待,也就是处于阻塞状态,这个时候 CPU 就空闲了,显然造成了 CPU 资源的浪费。这个时候如果在该进程内拉起另一个线程 T,那么 T 就可以见缝插针,在主线程等待数据的过程中, 把 CPU 的闲置资源利用起来,岂不美哉!这就是多线程技术的由来。
总有一些概念把人绕晕 -- 同步/异步、阻塞/非阻塞
在学习多线程技术的过程中,总有一些概念时不时蹦出来,比如同步/异步、阻塞/非阻塞,如果你是一位并发领域的高手,那么这些都是小儿科。如果你是一位初学者,那么你很可能陷入其中,不能自拔,从而最终放弃。接下来,我们尝试终结这个问题,带你爬出这个坑。
举个例子:
假如某一天你要去银行办理某项业务,到了银行,首先要凭身份证取个号。取完号之后,你发现这天人特别多,于是你就要等,等着被叫号。在等待的过程中,你可以有以下几个选择:
选择 1: 啥也不干,死盯着银行的液晶显示屏,等到出现你的号码时,你赶紧起身直奔柜台,银行叫号系统甚至还没来得及叫你的号。那么,在这段时间内,你就处于阻塞状态,而且你和银行叫号系统之间的关系是同步的。“阻塞”好理解,因为你啥也没干嘛!“同步”怎么理解呢?因为你和银行叫号系统的步调要保持一致,当显示屏上出现你的号时,你就得立马去柜台办业务,而银行自动广播喊你的功能对你来说,是没必要的。
选择 2:你还是啥也不干,但是你坐在座位上开始冥想,做着白日梦。因为你知道,轮到你的时候,银行叫号系统会广播叫你的。听到广播后,你再起身去柜台也不迟。那么在这段时间内,你还是处于阻塞状态。而你和银行叫号系统之间的关系是异步的,因为你不用再关注屏幕上滚动的号码,轮到你的时候,自然它会广播告诉你。
选择 3:对于一个聪明的人,显然不会坐在那傻等或者胡思乱想了。这时候,你拿起手机,决定打一把王者荣耀,打游戏的过程中,你还是时不时瞄一眼显示屏,等看到你的号码时,立马起身去柜台。那么这段时间内,你就处于非阻塞状态,因为你还干其他事了呀。但你和银行叫号系统间的关系还是同步的。
选择 4:如果你打算专心致志地玩一把王者荣耀,不想坑队友。那么你就等着广播叫你就可以了。那么这段时间内,你就处于非阻塞的状态,而且和银行叫号系统之间的关系还是异步的。
好了,总结一下:
所谓阻塞就是请求者发起读取数据的函数调用时,当数据还没准备好,这时如果函数一直在等待返回结果,就是阻塞;反之如果函数即刻返回,继续执行后面的动作就是非阻塞。
在 I/O 模型里,如果请求方从发起请求到数据最后准备好的这一段过程中都需要自己参与,那么这种我们称为同步请求;反之,如果应用发送完指令后就不再参与过程了,只需要等待最终完成结果的通知,那么这就属于异步。
哦,对了。上述选择 2 你也可以认为是 选择 4,因为你只要等待银行广播叫你就可以呀,做白日梦也可以认为是在干自己的事嘛!
再谈多线程
所谓多线程,就是多个线程在一个进程里同时运行,它们之间可以是协作的关系,一起完成某个共同的任务,也可以各干各的事。不管协作,还是单干,总有一个问题绕不开:“CPU 的资源怎么分配?”多线程之间争夺资源的过程可以认为是一种“零和博弈”,线程 A 抢夺的资源多,那么线程 B 得到的资源必然就少了嘛!一般来说,用户只会关心 CPU 总的利用率,这个值越大越好,而不用关注具体每个线程得到的计算资源是多是少。
多线程在同一个进程里跑,必然会带来一些问题,比如“多个线程要同时访问同一个变量,怎么办呢?”(各种锁)、“怎么控制线程的加入和退出呢?”(信号量)、“线程之间的 CPU 时间片怎么分配呢?”(资源调度模式)、“怎么创建一个线程呀?”(线程模板和线程池)等等。详细内容涉及的面就比较广了,JAVA、C++、C等不同的编程语言实现的方式也不尽相同,下次有时间再聊吧!