二进制程序,进程和线程
二进制程序(binaries)是指保存在存储介质上的程序,以给定操作系统和计算机体系结构可访问的格式编译生成,可以运行但尚未开始。进程(process)是操作系统对运行的二进制程序的抽象,包括:加载的二进制程序,虚拟内存,内核资源如打开的文件,关联的用户等。线程(threads)是进程内的执行单元,具体包括:虚拟处理器,堆栈,程序状态。换句话说,进程是正在运行的二进制程序,线程是操作系统调度器可以调度的最小执行单元。
一个进程包含一个或多个线程。如果一个进程只包含一个线程,则该进程只有一个执行单元,每次只有一个操作在运行。我们称这种进程为“单线程”,它是经典的UNIX进程。如果一个进程包含多个线程,每个会有多个操作在同时执行。我们称这种进程为“多线程”。
现代操作系统包含了两种对用户空间的基础的虚拟抽象:虚拟内存和虚拟处理器。它们使进程“感觉”自己独占机器资源。虚拟内存为每个进程提供独立的内存地址空间,该内存地址连续映射到物理RAM或磁盘存储(通过分页实现)。实际上,系统的RAM中可能有100个不同的正在运行的进程,但是每个进程都“感觉”所有的内存都是自己独占的。虚拟处理器使得进程“感觉”只有自己正在运行,操作系统对其“隐藏”了事实:多个进程在多个处理器(可能)以多任务方式同时运行。
虚拟内存是和进程相关的,与线程无关。因此,每个进程有独立的内存空间,而进程中的所有线程共享这份空间。相反地,虚拟处理器是和线程相关的,与进程无关。每个线程都是可独立调度的实体,支持单个进程每次“处理”多个操作。很多程序员会把虚拟内存和虚拟处理器搞混在一起,但从线程角度看,它们是完全不一样的。和进程一样,线程也“感觉”自己独占一个处理器。但是,和进程不同的是,线程并没有“感觉”主机独占内存——进程中的所有线程共享全部内存地址空间。
多线程
那么,为什么要有多线程呢?显然,我们需要进程,因为它们是正在运行的程序的抽象。但是,为什么要分离执行单元,引入线程?多线程机制提供了六大好处:
编程抽象
把工作切分成多个模块,并为每个分块分配一个执行单元(线程)是解决很多问题的常见方式。利用这种方法的设计模式包括“每个连接一个线程”和线程池模式。
并发性
对于有多个处理器的计算机,线程提供了一种实现“真正并发”的高效方式。每个线程有自己的虚拟处理器,是作为独立的调度实体,因此在多个处理器上可以同时运行多个线程,从而提高系统的吞吐量。由于线程可以实现并发性——也就是说,线程数小于等于处理器数。
提高相应能力
即使是在单处理器的计算机上,多线程也可以提高进程的响应能力。在单线程的进程中,一个长时间运行的任务会影响应用对用户输入的响应,导致应用看起来“僵死”了。有了多线程机制,这些操作可以委托给worker线程,至少有一个线程可以响应用户输入并执行UI操作。
I/O 阻塞
这和前一项“提高响应能力”紧密相关。如果没有线程,I/O 阻塞会影响整个进程。这对吞吐量和延迟都是灾难。在多线程的进程中,单个线程可能会因 I/O 等待而阻塞,而其他线程可以继续执行。除了线程之外,异步 I/O 和非阻塞 I/O 也是这种问题的解决方案。
上下文切换
在同一个进程中,从一个线程切换到另一个线程的代价要显著低于进程间的上下文切换。
内存保存
线程提供了一种可以共享内存,并同时利用多个执行单元的高效方式。从这个角度看,多线程在某些场景下可以取代多进程。
基于以上这些原因,多线程是操作系统以及应用的较常见的特性。在某些系统上,如Android,几乎每个进程都是多线程的。
上下文切换:进程和线程
线程的一大性能优势在于同一个进程内的线程之间的上下文切换代价很低(进程内切换)。在任何系统上,进程内切换的代价低于进程间的切换,前者通常是后者的一小部分。在非Linux系统上,这种代价差别非常明显,进程间通信代价非常高。因此,在很多系统上,称线程为”轻量级进程“。
在Linux系统中,进程间的切换代价并不高,而进程内切换的成本接近于0;接近进入和退出内核的代价。进程的代价不高,但是线程的代价更低。
计算机体系结构对进程切换有影响,而线程不存在这个问题。因为进程切换设计把一个虚拟地址空间切换到另一个虚拟地址空间。举个例子,在x86系统上,转换后备缓冲器(translation lookaside buffer,TLB),即用于把虚拟内存地址映射到物理内存地址的缓存,当切换到虚拟地址空间时,必须都清空。在某些负载场景下,TLB丢失对系统性能有极大损伤。在极端情况下,在某些ARM机器上,必须把整个CPU缓存都清空!对于线程而言,不存在这些代价,因为线程到线程之间的切换并不会交换虚拟地址空间。
多线程代价
虽然说多线程有很多优势,但也不是毫无代价。事实上,有些可怕的bug就是由多线程引起的。设计,编写,理解,以及最重要的——调试多线程程序,这些复杂度都远远高于单个线程的进程。
对线程恐惧的原因在于:多个虚拟的处理器,但是只有一个虚拟化内存实例。换句话说,多线程的进程有多个事件在同时运行(并发性),而这些事件共享同一块内存。自热而然地,同一个进程的线程会共享资源——也就是说,需要读或写同一份数据。因此,理解程序如何工作就从理解简单的序列化执行指令转变成对多线程独立运行的理解,时间和顺序不可预测,但结果肯定是正确的。如果线程同步失败,会造成脏数据,运行出错以及程序崩溃。由于理解和调试多线程代码非常困难,因此线程模型和同步策略必须从一开始就是系统设计的一部分。
其他选择
除了多线程外,还有一些其他选择,这取决于使用多线程的目的。比如,多线程带来的低延迟和高 I/O吞吐也可以通过 I/O 多路复用,非阻塞 I/O 和异步 I/O 来实现。这些技术支持进程并发执行 I/O 操作,不会阻塞进程。如果目的是实现并发,和 N 个线程相比, N 个进程也可以同样利用处理器资源,除了增加了资源消耗代价和上下文切换开销。相反,如果目的是减少内存使用,Linux提供了一些工具,相比起多线程,它们可以以更严格的方式共享内存。