基本术语
-
不变量:由程序作出的 假设,是程序中显式/隐式的一种 不变 的关系
举例来说,当我们有一个链表的时候,此时的不变量就可以是 每个节点都有一个指向下一个节点的next指针,所以当你删除或者增加节点时,需要维护这个指针
-
临界区:有时也叫 串行区域,指代影响 共享数据 的代码段,每一个临界区都至少对应一个数据不变量, 反之亦然
-
谓词:指代描述代码所需不变量的状态的语句,比如 当队列为空时,线程A...,谓词可以是布尔量、指针、复杂表达式等等,能够指示对所需不变量的状态要求即可
同步机制
同步其实就是在维护不变量,不变量经常会被破坏,而同步需要做的就是在被访问之前将其修复
原子操作
原子,即指不可分割
原子操作就是不可分割的操作,比如 x++ 这句代码,虽然只有一句,但他并非不可分割,也就是说并不是原子的,它会先将x 读入寄存器,然后再 加1,然后再 写回内存,如果在此期间有其他线程访问x则可能会出现非预期的结果
原子操作的实现:
- 硬件支持
- 互斥量
互斥量
互斥量基本概念
确保共享数据一次只能有 一个 线程访问,其他线程需要等待其释放,这可以通过 加锁解锁 实现
通常将互斥量和它要保护的数据定义在一起,如下:
struct tag {
pthread_mutex_t mutext;
int value;
};
当没有线程在这个互斥量上阻塞时,且该互斥量没有被加锁,那么可以立即释放它
互斥量的设计
互斥量的大小:并非指占用内存空间,指代的是 保护的串行区域 和 保护的共享数据
-
互斥量并非免费的,加锁和解锁会对性能造成影响,所以互斥量要 尽量少,保护的 串行区域要尽量大
-
互斥量本质是 串行执行,当保护的 串行区域 过大时,多个线程频繁访问会造成大量的等待时间(一次只能一个线程访问),所以互斥量要 足够多,每个互斥量保护的 串行区域要尽量小
这两种策略给出了各自的缺点,实际编程中可以从这两方面去考虑互斥量的设计
死锁

如上图所示,两个进程互相等待对方释放资源,形成了一个循环
死锁发生通常需要满足以下四个条件:
-
互斥条件:每个资源都只能被一个线程占用
-
持有并等待条件:一个线程持有至少一个资源,并等待获取其他资源,这些资源当前被其他线程持有
-
不剥夺条件:线程已获得的资源不能被强制剥夺,必须由线程自行释放
-
循环等待条件:在一个资源的请求链中,形成了一个循环等待的情况
解决办法:(破坏四个条件中的一个即可)
-
固定加锁层次:固定加锁顺序,要锁住B,必须先锁住A
-
试加锁和回退:在 正常锁住 集合中的某一个互斥量后,尝试锁住 集合中的其他互斥量,如果失败,则 释放 所有锁住的互斥量,重新加锁
拓展:
-
层次锁:通过 定义锁的层次关系 来避免死锁的策略。在这种策略下,系统会对所有锁按某种顺序进行排列,并要求线程按照从低到高的顺序获取锁
-
链锁:在链锁中,线程获取锁的顺序不是固定的,而是根据 资源之间的“链”或依赖关系 来动态决定(可以释放之前的互斥量)
| 特性 | 层次锁 (Hierarchical Locking) | 链锁 (Chain Locking) |
|---|---|---|
| 死锁避免 | 通过严格的锁顺序避免死锁 | 通过动态调整锁顺序,依赖关系明确时也能避免死锁 |
| 灵活性 | 较低,锁的顺序是固定的 | 高,锁顺序根据资源依赖关系动态决定 |
| 实现难度 | 相对简单,易于理解和实现 | 较复杂,需要动态管理锁的依赖关系 |
| 适用场景 | 适合资源间依赖关系较简单且需要明确顺序获取锁的场景 | 适合资源间依赖关系复杂、动态调整锁获取顺序的场景 |
条件变量
概念
条件变量:
-
是用来通知共享数据状态信息的
-
是与互斥量以及互斥量保护的共享数据相关的信号
-
是程序用来等待某个谓词为真的机制
-
不提供互斥,所以需要和一个互斥量绑定
-
每个条件变量都必须和一个特定的互斥量、一个谓词相关联
解锁和等待操作必须是原子的,这是因为,当线程A需要等待某个条件时,将互斥量解锁,然后陷入等待,如果这不是原子的,在解锁之后阻塞之前,线程B修改了状态,然后查找等待队列看是否需要唤醒其他线程,但此时线程A还未进入等待队列,所以就可能永远不会被唤醒(即使条件已经满足)。故引入了条件变量,它让二者是原子的
线程在等待条件变量时,它必须将相关互斥量锁住。在线程阻塞之前,条件变量操作将解锁互斥量,在它重返线程之前,它会再次锁住互斥量
条件变量在任何时刻只能和一个互斥量相关联,反之不然
条件变量必须在没有任何线程在等待它时销毁。并且通常是 先销毁条件变量,再销毁互斥量
信号与广播
-
信号
唤醒 一个 正在等待条件变量的线程。如果有多个线程在等待条件变量,只有其中一个线程会被唤醒
-
广播
唤醒 所有 等待条件变量的线程。所有等待的线程都会被唤醒,并且它们会竞争获得互斥锁,并继续执行
在信号和广播前,应该先锁住互斥量,如果不锁住很可能会造成信号丢失
首先,我们要知道,当没有线程等待条件变量时,pthread_cond_signal 不会做任何事情,并且它不会保存信号。也就是说,信号是一次性的。假设线程A没有锁住互斥量,直接更改条件并发送信号,而线程B很有可能在线程A发送信号之后还没有进入等待队列,因此 信号丢失,永远陷入阻塞等等
所以在信号和广播前锁住互斥量可以保证执行的顺序(即 线程A在线程B释放锁陷入等待之后才会发送信号)
测试谓词
总是测试你的谓词,然后再次测试它
采用 循环 来检查条件,而不是简单地等待一次通知
while (!condition) {
cond_wait(&cond, &mutex);
}
原因:
如果线程不在循环中检查条件,那么它可能会在条件未满足时继续执行,这可能导致竞争条件或不一致的状态。使用循环可以确保每次线程被唤醒时,只有在条件满足时才继续执行,从而避免了潜在的问题
- 假唤醒:线程等待条件变量时,可能会无缘无故被唤醒,即使没有其他线程显式地通知它
总结
建议你将一组不变量、谓词和他们的互斥量,以及一个或多个的条件变量封装为一个数据结构的元素,并仔细地记录下他们之间的关系
posted on
浙公网安备 33010602011771号