1. 线程的基本概念
什么是线程?
- 定义:线程是进程中的一个执行流,是CPU调度和执行的基本单位。
- 进程与线程的关系:
- 进程是资源分配的单位(拥有代码、数据、文件等资源)。
- 线程是进程内部的一个实体,它共享进程的所有资源,但拥有自己独立的执行轨迹(程序计数器、寄存器集合、栈)。
- 进程是资源分配的单位(拥有代码、数据、文件等资源)。
单线程 vs. 多线程进程
- 单线程进程:传统进程,只有一个执行流。
代码 + 数据 + 文件 + 1组(寄存器、栈)
- 多线程进程:一个进程内包含多个执行流。
代码 + 数据 + 文件 + N组(寄存器、栈)
经典比喻:
*
进程就像一个厨房,它拥有所有的资源:空间、水、电、厨具(对应代码、数据、文件等资源)。
*
单线程进程是厨房里只有一个厨师,他负责所有工作。
*
多线程进程是厨房里有多个厨师(线程),他们共享厨房的所有资源,但各自进行不同的任务(比如一个切菜,一个炒菜),协同完成一顿饭。
2. 使用多线程的好处
- 响应性:即使在单核CPU上,一个线程在等待I/O时,另一个线程可以继续执行,保持用户界面的响应。
- 例子:在字处理软件中,一个线程处理用户输入,另一个线程在后台进行拼写检查,你不会因为检查拼写而无法打字。
- 例子:在字处理软件中,一个线程处理用户输入,另一个线程在后台进行拼写检查,你不会因为检查拼写而无法打字。
- 资源共享:线程默认共享它们所属进程的内存和资源。创建和切换线程比创建进程的代价小得多。
- 经济性:创建和上下文切换线程比进程更快,消耗资源更少。
- 可伸缩性:在多处理器体系结构上,多线程可以真正并行运行在不同的CPU核上,从而显著提高性能。
3. 用户线程与内核线程
这是理解线程实现的关键。
用户线程
- 管理方:在用户态由线程库(如Pthreads,
Win32 threads, Java
threads)进行管理,内核并不知道它们的存在。
- 优点:
- 创建、管理、切换速度极快,因为不需要进入内核态。
- 创建、管理、切换速度极快,因为不需要进入内核态。
- 缺点:
- 如果一个用户线程执行了阻塞式系统调用,整个进程都会被阻塞,因为内核以为这个进程只有一个执行流。
- 无法利用多核CPU实现真正的并行。
- 如果一个用户线程执行了阻塞式系统调用,整个进程都会被阻塞,因为内核以为这个进程只有一个执行流。
内核线程
- 管理方:由操作系统内核直接支持和管理。
- 优点:
- 当一个线程阻塞时,内核可以调度该进程内的另一个就绪线程或其他进程的线程。
- 可以在多处理器上真正并行执行。
- 当一个线程阻塞时,内核可以调度该进程内的另一个就绪线程或其他进程的线程。
- 缺点:
- 线程的创建、管理和切换都需要系统调用,代价比用户线程大。
4. 多线程模型
用户线程和内核线程需要通过某种方式关联起来,这就产生了三种模型。
1. 多对一模型
- 映射:多个用户线程映射到一个内核线程。
- 特点:
- 线程管理在用户空间进行,效率高。
- 致命缺点:一个用户线程的阻塞系统调用会导致整个进程阻塞。无法在多处理器上并行。
- 线程管理在用户空间进行,效率高。
- 现状:基本已被淘汰。
2. 一对一模型
- 映射:一个用户线程映射到一个内核线程。
- 特点:
- 当一个线程阻塞时,其他线程可以继续运行。
- 允许在多个处理器上并行执行多个线程。
- 缺点:创建用户线程就需要创建内核线程,开销较大,可能会限制系统支持的线程数量。
- 当一个线程阻塞时,其他线程可以继续运行。
- 例子:Windows, Linux。
3. 多对多模型
- 映射:多个用户线程(数量不限)映射到数量相当或更少的内核线程上。
- 特点:
- 克服了多对一模型并发度低的缺点,也克服了一对一模型开销大的缺点。
- 内核可以创建足够数量的内核线程以供利用CPU,同时用户也可以创建任意数量的用户线程。
- 克服了多对一模型并发度低的缺点,也克服了一对一模型开销大的缺点。
- 变种:两级模型:是多对多模型的一种变体,允许一个用户线程绑定到一个特定的内核线程上(为某些重要线程提供保障)。
5. 线程相关的议题
fork()和exec()的语义:- 当一个多线程进程调用
fork()时,是只复制调用fork()的线程,还是复制所有线程?不同的系统有不同的实现。
- 如果
fork()之后立即调用exec(),则复制所有线程是浪费的,因为exec()会用新程序覆盖整个地址空间。
- 当一个多线程进程调用
- 线程取消:
- 异步取消:一个线程立即终止目标线程。
- 延迟取消:目标线程周期性地检查是否应该终止,允许它进行必要的清理工作(更安全)。
- 异步取消:一个线程立即终止目标线程。
- 信号处理:
- 信号是发给进程的还是某个特定线程的?
- 多线程环境中,信号的处理非常复杂。
- 信号是发给进程的还是某个特定线程的?
- 线程池:
- 思想:在进程启动时创建一定数量的线程,放入“池”中待命。当有任务到来时,从池中唤醒一个线程来执行,执行完毕后返回池中。
- 优点:
- 比需要时再创建新线程更快。
- 限制了系统中并发线程的总数,防止资源耗尽。
- 比需要时再创建新线程更快。
- 思想:在进程启动时创建一定数量的线程,放入“池”中待命。当有任务到来时,从池中唤醒一个线程来执行,执行完毕后返回池中。
- 线程特定数据:
- 允许每个线程拥有属于自己的数据副本,尽管代码是共享的。
- 例子:全局变量
errno。在多线程中,如果errno是真正的全局变量,一个线程可能会读取到另一个线程设置的错误码。使用线程特定数据,每个线程都有自己的errno副本。
- 允许每个线程拥有属于自己的数据副本,尽管代码是共享的。
6. 不同系统中的线程实现
Pthreads
- 一个POSIX标准的线程API,定义了线程操作的行为规范,而不是具体实现。
- 可以在用户级或内核级实现,常见于UNIX-like系统(Solaris, Linux, Mac OS X)。
Windows线程
- 采用一对一模型。
- 每个线程包含:
- 线程ID
- 寄存器组
- 独立的用户栈和内核栈
- 私有存储区域
- 线程ID
- 以上这些统称为线程的上下文。
Linux线程
- Linux将线程视为一种能共享资源的特殊进程,称之为任务。
- 使用
clone()系统调用创建线程,通过传递不同的参数标志,可以控制子任务与父任务共享哪些资源(如地址空间、文件描述符等)。
Java线程
- Java线程是JVM的一部分,通常通过一对一模型映射到底层操作系统的原生线程。
- 创建方式:
- 继承
Thread类。
- 实现
Runnable接口(更推荐,因为Java不支持多继承)。
- 继承
- 线程状态:
- 新建:被创建,尚未启动。
- 可运行:可能在运行,也可能在等待CPU。
- 阻塞:等待一个监视器锁(同步)。
- 等待:无限期等待另一个线程执行特定动作。
- 超时等待:在指定时间内等待。
- 终止:线程已执行完毕。
- 新建:被创建,尚未启动。
如果您喜欢我的文章,可以考虑打赏以支持我继续创作.