Thread
线程概述
为使程序能并发执行,系统必须进行以下的一系列操作:创建进程、撤消进程、进程切换
由于进程是一个资源的拥有者,因此在创建、撤销和切换中,系统必须为此付出较大的时间和空间的开销。
如何使多个程序更好的并发执行,同时又能减少系统开销?
进程的概念体现出两个特点:
- 资源所有权:一个进程包括一个保存进程映像的虚地址空间,并且随时分配对资源的控制或所有权,包括内存、I/O 通道、I/O 设备、文件等。
- 调度/执行:进程是被操作系统调度的实体。
调度和分派的部分通常称为线程或轻型进程(lightweight process),而资源所有权的部分通常称为进程
线程具有许多传统进程所具有的特征,所以又称为轻型进程(Light-Weight Process) ,相应地把传统进程称为重型进程(Heavy-Weight Process),传统进程相当于只有一个线程的任务。
在引入了线程的操作系统中,通常一个进程都拥有若干个线程,至少也有一个线程。
History
- 60 年代:提出进程(Process)概念,在 OS 中一直都是以进程作为能拥有资源和独立运行的基本单位的。
- 80 年代中期:提出线程(Thread)概念,线程是比进程更小的能独立运行的基本单位,目的是提高系统内程序并发执行的程度,进一步提高系统的吞吐量。
- 90 年代:多处理机系统得到迅速发展,线程能比进程更好地提高程序的并行执行程度,充分发挥多处理机的优越性。
线程的共享问题
进程内的所有线程共享进程的很多资源,而这种共享又带来了同步问题
WIKITEXT
线程间共享 线程私有进程指令 线程ID全局变量 寄存器集合(包括PC和栈指针)打开的文件 栈(用于存放局部变量)信号处理程序 信号掩码当前工作目录 优先级用户ID
线程的互斥问题
对全局变量进行访问的基本步骤
- 将内存单元中的数据读入寄存器
- 对寄存器中的值进行运算
- 将寄存器中的值写回内存单元
线程的属性
- 轻型实体 线程自己基本不拥有系统资源,只拥有少量必不可少的资源:TCB,程序计数器、一组寄存器、栈。
- 独立调度和分派的基本单位 在多线程 OS 中,线程是独立运行的基本单位,因而也是独立调度和分派的基本单位。
- 可并发执行 同一进程中的多个线程之间可以并发执行,一个线程可以创建和撤消另一个线程。
- 共享进程资源 它可与同属一个进程的其它线程共享进程所拥有的全部资源。
线程的控制
Create/Terminate Thread
线程的创建:
- 在多线程 OS 环境下,应用程序在启动时,通常仅有一个“初始化线程”线程在执行。
- 在创建新线程时,需要利用一个线程创建函数(或系统调用),并提供相应的参数。
- 如指向线程主程序的入口指针、堆栈的大小,以及用于调度的优先级等。
- 在线程创建函数执行完后,将返回一个线程标识符供以后使用。
线程的终止:
- 线程完成了自己的工作后自愿退出;
- 或线程在运行中出现错误或由于某种原因而被其它线程强行终止。
线程的三种终止方式
- 线程从启动例程函数中返回,函数返回值作为线程的退出码
- 线程被同一进程中的其他线程取消
- 线程在任意函数中调用
pthread_exit
函数终止执行
线程间的同步与通信
互斥锁 Mutex
保证同一时间只有一个线程能访问共享资源,其他线程必须等待当前线程释放锁后才能获取访问权。操作互斥锁的时间和空间开销都较低,因而较适合于高频度使用的关键共享数据和程序段。
- 线程访问共享资源前,必须先 “获取锁”:
- 若锁未锁定,线程成功获取锁(锁变为锁定状态),可访问资源。
- 若锁已锁定,线程会被阻塞(进入等待队列),直到锁被释放。
- 线程访问完资源后,必须 “释放锁”(锁变回未锁定状态),让等待队列中的线程竞争获取。
- 适用场景:共享资源的读写操作都需要独占;例如多线程抢购商品时,“库存减 1” 操作必须通过互斥锁保证原子性,避免超卖。
条件变量 Condition Variable
在多线程编程中,条件变量是一种同步机制,用于在某个特定条件变为真时唤醒一个或多个线程。条件变量通常与互斥锁(mutex)一起使用,以确保线程在检查条件和对条件进行等待时不会发生竞争条件。
以下是条件变量在控制线程同步中的一般用法:
- 等待条件:当线程需要等待某个条件变为真时,它会首先获取与条件变量相关联的互斥锁。然后,它会检查条件是否已经满足。如果条件未满足,线程会调用条件变量的等待方法(例如在 Java 中的
Condition.await()
)。这将释放互斥锁,并使线程进入睡眠状态,等待条件变为真。 - 改变条件:当线程改变可能会影响条件的状态的数据时,它会首先获取互斥锁,然后修改数据。修改完成后,它会调用条件变量的唤醒方法(例如在 Java 中的
Condition.signal()
或Condition.signalAll()
),以唤醒正在等待该条件的所有线程。然后,它会释放互斥锁。 - 响应唤醒:当线程被唤醒时(即,它从等待方法返回时),它会重新获取互斥锁,并再次检查条件。这是必要的,因为在多线程环境中,条件可能在线程被唤醒和线程实际运行之间的时间内发生变化。
使用条件变量可以使线程在等待某个条件时不必进行忙等待(即,不断地检查条件是否已经满足)。这可以大大提高系统的效率,因为线程在等待时不会消耗 CPU 资源。
Semephore
- 私用信号量(private semaphore)。
当某线程需利用信号量来实现同一进程中各线程之间的同步时,可调用创建信号量的命令来创建一私用信号量,其数据结构存放在应用程序的地址空间中。
私用信号量属于特定的进程所有,OS 并不知道私用信号量的存在,因此,一旦发生私用信号量的占用者异常结束或正常结束,但并未释放该信号量所占有空间的情况时,系统将无法使它恢复为 0(空),也不能将它传送给下一个请求它的线程。 - 公用信号量(public semaphore)。
公用信号量是为实现不同进程间或不同进程中各线程之间的同步而设置的。
有着一个公开的名字供所有的进程使用,故称为公用信号量。
其数据结构是存放在受保护的系统存储区中,由 OS 为它分配空间并进行管理,故也称为系统信号量
如果信号量的占有者在结束时未释放该公用信号量,则 OS 会自动将该信号量空间回收,并通知下一进程。因此公用信号量是一种比较安全的同步机制
ULT & KLT
对比维度 | 用户级线程(ULT,User-Level Thread) | 内核级线程(KLT,Kernel-Level Thread) |
---|---|---|
管理主体 | 由用户态线程库调度 | 由OS内核调度 |
内核可见性 | 不可见(内核仅将进程视为调度单位) | 可见(内核直接管理线程,作为独立调度单位) |
创建 / 销毁 / 切换开销 | 极低(用户态操作,无需系统调用) | 中等(需系统调用进入内核态处理) |
并行能力 | 1. 单个线程阻塞会导致整个进程阻塞 2. 无法利用多 CPU,同一进程内的ULT只能在单个核心上轮流执行 | 1. 单个线程阻塞不影响其他线程执行 2. KLT可被内核分配到不同 CPU 核心 |
同步机制实现 | 依赖用户态库(如用户态锁),效率高但易出错 | 依赖内核提供的同步机制(如内核锁),安全性更高 |
移植性 | 强(不依赖特定内核,线程库可跨系统使用) | 弱(依赖内核线程实现,不同系统可能存在差异) |
典型应用场景 | 简单并发任务、阻塞较少的场景、资源受限设备 | 多 CPU 系统、IO 密集型任务、需要高可靠性的并发场景 |
示例 | 早期 Python thread 模块、某些嵌入式系统线程库 | Java Thread 、C++ std::thread 、Linux Pthreads |