该系列主要作为曾经的纯前端,对后台和底层的一些弥补,涉及进程、网络通信,以及对 node.js 和相关框架的学习。本节我们学习一下单线程与多线程、阻塞、同步/异步和 IO 等内容。
# 围绕线程
# 多线程
多线程,即便处理器只能运行一个线程,操作系统也可以通过快速的在不同线程之间进行切换,由于时间间隔很小,来给用户造成一种多个线程同时运行的假象。这样的程序运行机制被称为软件多线程。
# 多线程的假象
大部分操作系统都支持多进程并发运行,现代的操作系统几乎都支持同时运行多个任务对于一个 CPU 而言,它在某个时间点上只能执行一个程序,也就是说,只能运行一个进程,CPU 不断地在这些进程之间轮换执行。
因为 CPU 的执行速度快,所以 CPU 在多个进程之间轮换执行,但感觉到(宏观上)好像多个进程在同时执行。当然,如果启动的程序足够多,依然可以感觉到程序的运行速度下降。
现代的操作系统都支持多进程的并发(轮换执行),但在具体的实现细节上可能因为硬件和操作系统的不同而采用不同的策略。目前操作系统大多采用效率更高的抢占式多任务策略。
# 多线程的优点
- 进程之间不能共享内存,但同一进程中的线程之间共享内存非常容易
- 系统创建进程需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高
- 多线程技术使程序的响应速度更快,因为用户界面可以在进行其它工作的同时一直处于活动状态
# 多线程的缺点
- 等候使用共享资源时造成程序的运行速度变慢。这些共享资源主要是独占性的资源,如打印机等
- 对线程进行管理要求额外的 CPU 开销。线程的使用会给系统带来上下文切换的额外负担。当这种负担超过一定程度时,多线程的特点主要表现在其缺点上,比如用独立的线程来更新数组内每个元素
- 线程的死锁。即较长时间的等待或资源竞争以及死锁等多线程症状
# 多线程 VS 多进程
因为并不是说所有情况下用多线程都是好事,因为多线程的情况下,CPU 还要花时间去维护,CPU 处理各线程的请求时在线程间的切换也要花时间,所以一般情况下是可以不用多线程的,用了有时反而会得不偿失。大多情况下,要用到多线程的主要是需要处理大量的 IO 操作时或处理的情况需要花大量的时间等等,比如:读写文件、视频图像的采集、处理、显示、保存等。
现代的体系,一般 CPU 会有多个核心,而多个核心可以同时运行多个不同的线程或者进程。
当每个 CPU 核心运行一个进程的时候,由于每个进程的资源都独立,所以 CPU 核心之间切换的时候无需考虑上下文。当每个 CPU 核心运行一个线程的时候,由于每个线程需要共享资源,所以这些资源必须从 CPU 的一个核心被复制到另外一个核心,才能继续运算,这占用了额外的开销。换句话说,在 CPU 为多核的情况下,多线程在性能上不如多进程。
# 单线程
说到单线程,身为 Javascript 出身的你我可能再熟悉不过了。
如果有很多任务需要执行,不外乎三种解决方法。
- 排队。因为一个进程一次只能执行一个任务,只好等前面的任务执行完了,再执行后面的任务。
- 新建进程。为每个任务新建一个进程。
- 新建线程。因为进程太耗费资源,所以如今的程序往往允许一个进程包含多个线程,由线程去完成任务。
以 JavaScript 语言为例,它是一种单线程语言,所有任务都在一个线程上完成,即采用上面的第一种方法。一旦遇到大量任务或者遇到一个耗时的任务,网页就会出现"假死",因为 JavaScript 停不下来,也就无法响应用户的行为。
# 单线程缺点
如上所说,使用单线程,当执行某个耗时或者不能立即完成的任务时,比如:网络通讯、复杂运动,该线程就会暂时停止对其他任务的响应和处理,造成的视觉效果就是程序的“假死”,也就是应用程序被卡在那里无法继续执行。
# 阻塞与非阻塞、同步与异步
# 阻塞与非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
# 同步与异步
同步是指代码调用 IO 操作时,必须等待 IO 操作完成才返回的调用方式。
异步是指代码调用 IO 操作时,不必等 IO 操作完成就返回的调用方式。
同步是最原始的调用方式,异步则需要多线程,多 CPU 或者非阻塞 IO 的支持。
# 区分
- 同步与异步是关于指令执行顺序的,阻塞非阻塞是关于线程与进程的。
- 同步和异步关注的是消息通信机制,阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。
骚年也来学学讲故事吧--小白拿外卖的故事:
- 同步阻塞:小白点了外卖,在餐馆等,看到外卖好了拿走(小白干等,主动查看外卖进展)
- 同步非阻塞:小白点了外卖,然后去买饮料、买水果,时不时回来看看,等外卖好了拿走(小白不用等,主动查看外卖进展)
- 异步阻塞:小白点了外卖,在餐馆等,外卖好了服务员通知让他拿走(小白干等,外卖进展自动通知)
- 异步非阻塞:小白点了外卖,然后去买饮料、买水果,外卖好了服务员跑来通知让他拿走(小白不用等,外卖进展自动通知)
# IO
IO 是输入 input 输出 output 的首字母缩写形式,直观意思是计算机输入输出,它描述的是计算机的数据流动的过程,因此 IO 第一大特征是有数据的流动。
从计算机架构上来讲,任何涉及到计算机核心(CPU 和内存)与其他设备间的数据转移的过程就是 IO。本体就是计算机核心(CPU 和内存)。例如从硬盘上读取数据到内存,是一次输入,将内存中的数据写入到硬盘就产生了输出。在计算机的世界里,这就是 IO 的本质。
UNIX 中有五种 IO 模型,下面用两个阶段说明:
- 等待数据
- 拷贝数据
# 阻塞式 IO(blocking IO)
默认情况下,Linux 下的所有 socket 都是阻塞的。
在 I/O 执行的两个阶段都被阻塞了——阻塞等待数据,阻塞拷贝数据。
# 非阻塞式 IO(non-blocking IO)
当对一个非阻塞 socket 执行读操作时,如果内核中的数据还没有准备好,那么它并不会阻塞用户进程,而是立刻返回一个 EWOULDBLOCK 错误。
在 I/O 执行的第一个阶段不会阻塞线程,但在第二阶段会阻塞。
# IO 复用(IO multiplexing)
也称事件驱动 IO(event-driven IO),就是在单个线程里同时监控多个套接字,通过select
或poll
轮询所负责的所有 socket,当某个 socket 有数据到达了,就通知用户进程。
进行了两次系统调用,进程先是阻塞在 select/poll 上,再是阻塞在读操作的第二个阶段上。
# 信号驱动式 IO(signal driven IO)
让内核在描述符就绪时发送 SIGIO 信号通知用户进程。
在等待数据 ready 期间进程不被阻塞,当收到信号通知时再阻塞并拷贝数据。
# 异步 IO(asynchronous IO)
用户进程在发起 aio_read 操作后,该系统调用立即返回。然后内核会自己等待数据 ready,并自动将数据拷贝到用户内存。整个过程完成以后,内核会给用户进程发送一个信号,通知 IO 操作已完成。
异步 IO 的特点是 IO 执行的两个阶段都由内核去完成,用户进程无需干预,也不会被阻塞。
五种 IO 模型的比较:
前 4 种模型的主要区别在于第一阶段,因为它们的第二阶段是一样的:都是阻塞于 recvfrom 调用,将数据从内核拷贝到用户进程缓冲区。
# 参考
- 《单线程和多线程的简单理解》 (opens new window)
- 多线程- 维基百科 (opens new window)
- 怎样理解阻塞非阻塞与同步异步的区别? (opens new window)
- 《同步,异步,阻塞,非阻塞等关系轻松理解》 (opens new window)
- 《程序员应该这样理解 IO》 (opens new window)
- 《IO 模型:同步、异步、阻塞、非阻塞》 (opens new window)
- 《单线程和多线程的优缺点》 (opens new window)
# 结束语
这一节主要作为对线程的补充,收集了相关的一些 IO 等的资料。
身为一个 JSer,对多线程、阻塞、同步等的理解都很少,整理笔记的同时也强迫学习了一波。