基于ssh框架的网站开发流程,宁波建设工程学校网站,建网站上海,吕邵苍设计公司网站目录 一、线程互斥 1. 线程互斥的引入 1.1 模拟抢票代码 1.2 错误发生的时间流程分析(⭐⭐⭐) 1.2.1 卖出负数票(if 判断与执行间的空隙导致) 1.2.2 卖出重复票(由 tickets-- 的非原子性导致) 2. 互斥量Mutex 3. 互斥量的接口 3.1 初始化互斥量 3.1.1 静态分配#…目录一、线程互斥1. 线程互斥的引入1.1 模拟抢票代码1.2 错误发生的时间流程分析(⭐⭐⭐)1.2.1 卖出负数票(if 判断与执行间的空隙导致)1.2.2 卖出重复票(由 tickets-- 的非原子性导致)2. 互斥量Mutex3. 互斥量的接口3.1 初始化互斥量3.1.1 静态分配宏定义法3.1.2 动态分配函数法3.2 互斥量加锁3.3 互斥量解锁3.4 销毁互斥量4.互斥量的实现原理4.1 前置知识4.2 伪代码分析4.2.1 lock 原理(⭐⭐⭐)4.2.2 unlock 原理4.2.3 lock中途发生进程切换5. 互斥量的RAII 封装5.1 传统手动加解锁的缺陷5.2 RAIIResource Acquisition Is Initialization资源获取即初始化二、可重入函数与线程安全1. 概念1.1 可重入1.2 线程安全2. 常见情况2.1 常见可重入情况2.2 常见线程安全情况3. 联系与区别3.1 联系3.2 区别4. 常见误区(⭐⭐⭐)三、死锁1. 概念2. 必要条件3. 避免死锁的方式3.1 一次性分配(破坏“请求与保持”)3.2 主动退让(破坏“不剥夺”)3.3 按序加锁(破坏“循环等待”)3.4 银行家算法四、线程同步1. 竞态条件与同步概念1.1 竞态条件1.2 线程同步2. 条件变量3. 条件变量函数(与互斥量非常相似)3.1 初始化3.1.1 静态初始化3.1.2 动态初始化3.2 条件变量等待3.3 条件变量唤醒(Signal / Broadcast)3.4 销毁条件变量4. pthread_cond_wait 中的互斥量(⭐⭐⭐)5. 条件变量使用规范5.1 等待线程的写法(⭐⭐⭐)5.2 唤醒线程的写法五、自旋锁1. 概念2. 使用场景3. POSIX 自旋锁 API 函数4. 易错点一、线程互斥1. 线程互斥的引入1.1 模拟抢票代码#include iostream #include pthread.h #include unistd.h #include vector #include string using namespace std; // 全局变量所有线程共享的数据区 int tickets 100; void *getTicket(void *args) { // 精简直接将传进来的整形强制转换省去 new 结构体 long thread_id (long)args; string name thread- to_string(thread_id); while (true) { // 步骤 1判断是否有票 if (tickets 0) { // 步骤 2模拟业务耗时查询数据库、网络延迟等制造时间窗口 usleep(1000); // 步骤 3打印卖票信息并执行减票操作 printf(Who: %s, get a ticket: %d\n, name.c_str(), tickets); tickets--; } else { break; // 没票了退出 } } printf(%s ... quit\n, name.c_str()); return nullptr; } int main() { const int NUM 4; vectorpthread_t tids(NUM); // 创建 4 个抢票线程 for (long i 1; i NUM; i) { pthread_create(tids[i - 1], nullptr, getTicket, (void *)i); } // 等待所有线程结束 for (int i 0; i NUM; i) { pthread_join(tids[i], nullptr); } return 0; }1.2 错误发生的时间流程分析(⭐⭐⭐)1.2.1 卖出负数票(if判断与执行间的空隙导致)这是usleep(1000)故意放大的灾难。假设此时只剩最后1张票tickets 1时刻 1thread-1检查if (tickets 0)结果为真。进入if块执行usleep线程休眠让出 CPU。时刻 2thread-2抢到 CPU检查if (tickets 0)此时内存里的票还是 1结果为真。进入if块也执行usleep让出 CPU。时刻 3thread-3抢到 CPU同样检查为真进入if块休眠。时刻 4thread-1醒来打印卖出第1张票执行tickets--内存中tickets变成0。时刻 5thread-2醒来它不会再去检查条件了因为它已经通过了if检查直接打印卖出第0张票执行tickets--内存中tickets变成-1。时刻 6thread-3醒来打印卖出第-1张票tickets变成-2。1.2.2 卖出重复票(由tickets--的非原子性导致)假设此时tickets 10没有usleep的影响时刻 T1thread-1执行tickets--的第一步将内存中的10读入自己的寄存器。突然时间片耗尽thread-1被强行切走寄存器上下文被保存。时刻 T2thread-2开始执行它从内存读出10运算减 1 变成9写回内存。现在内存里tickets是9。时刻 T3thread-1恢复运行恢复自己的寄存器状态里面存的还是10运算减 1 变成9写回内存。内存里的tickets依然是9。结果两个线程明明都卖出了一张票但总票数只减少了 1。这在真实商业系统中就是严重的“超卖”事故。2. 互斥量Mutex操作共享变量的这段代码区域在计算机科学中被称为临界区 (Critical Section)。为了彻底解决并发带来的数据竞争问题我们需要为临界区立下三条必须遵守的铁律绝对互斥当一个线程已经进入临界区执行时必须把门“锁死”绝对不允许其他任何线程闯入。竞争唯一如果多个线程同时在门外排队要求进入且当前临界区空闲无人那么系统必须保证只能放行一个线程进去不能一拥而上。互不干涉如果一个线程正在临界区之外执行即在处理它自己的私有逻辑它绝对不能阻塞或干扰其他想要进入临界区的线程。在 Linux 系统的底层编程中这把专门用来保护临界区、维持多线程秩序的锁就叫做互斥量Mutex Mutual Exclusion 的缩写。通过对互斥量进行“加锁”和“解锁”操作我们就能完美驯服狂野的并发线程让它们乖乖排队访问共享资源。3. 互斥量的接口3.1 初始化互斥量在 Linux 系统中互斥量的数据类型是pthread_mutex_t。当定义了这样一个变量时它最初只是一块未经雕琢的内存。要让操作系统将其识别为一把有效的锁必须进行初始化。初始化互斥量有两种完全不同的方式静态初始化和动态初始化。3.1.1 静态分配宏定义法如果互斥量被定义为全局变量或者静态static变量可以直接使用系统提供的宏来一键初始化pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER;特点极其简单不需要写初始化函数也不需要后续手动销毁。但它只能使用默认属性且不能用于局部变量或动态分配new/malloc的内存。3.1.2 动态分配函数法如果互斥量是局部变量、动态申请在堆上的或者被封装在 C 类内部就必须调用系统 API 进行动态初始化int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);mutex指向你需要初始化的互斥量对象的指针。attr互斥量的属性设置。通常我们不需要特殊的属性直接传NULL即可表示使用默认属性。返回值成功返回 0失败返回错误码。3.2 互斥量加锁当线程准备进入临界区操作共享变量时必须先调用此函数“抢锁”。int pthread_mutex_lock(pthread_mutex_t *mutex);核心行为机制如果锁是空闲的当前线程顺利拿到锁函数立即返回 0线程继续往下执行进入临界区。如果锁已经被别人拿了当前线程会被操作系统阻塞挂起/睡眠。它交出 CPU 执行权在原地排队直到那把锁被别人解开它才会被唤醒并再次尝试抢锁。补充非阻塞加锁pthread_mutex_trylock如果你不希望线程傻等可以使用trylock。它的区别在于如果发现锁被别人拿了它不会睡眠挂起而是立刻返回一个错误码通常是EBUSY让线程可以先去干点别的事情。3.3 互斥量解锁当线程执行完临界区的代码后必须立刻解锁把资源让出来。int pthread_mutex_unlock(pthread_mutex_t *mutex);核心行为机制将互斥量的状态从“已锁定”改为“未锁定”。如果此时有其他线程正因为调用lock而排队睡眠操作系统会唤醒其中一个或多个取决于调度策略线程让它们去竞争这把刚刚释放的锁。加解锁铁律谁加锁谁解锁绝对不能 A 线程加的锁让 B 线程去解。严禁“二次加锁”同一个线程在没有解锁的情况下再次对同一把锁调用lock会导致自己被自己阻塞形成死锁。3.4 销毁互斥量当程序运行到尾声或者业务对象被销毁时如果互斥量不再被使用了必须将其清理干净将系统资源归还给 OS。int pthread_mutex_destroy(pthread_mutex_t *mutex);参数传入你要销毁的互斥量指针。返回值成功返回 0失败返回错误码。销毁互斥量的三大重点静态锁免死使用PTHREAD_MUTEX_INITIALIZER静态初始化的互斥量不需要也不应该被 destroy。锁定状态禁毁绝对不要销毁一个当前正处于“加锁”状态的互斥量否则会导致不可预期的系统错误通常返回EBUSY。有人排队禁毁如果还有其他线程正阻塞在这把锁上还在等锁此时销毁锁会导致灾难性后果。必须确保所有相关线程都已经退出或不再使用该锁后再进行销毁。4.互斥量的实现原理在前面的抢票例子中我们知道tickets--不是原子操作从而导致了数据错乱。那么问题来了既然连一行简单的 C 代码都不是原子的那操作系统是靠什么保证lock和unlock本身是安全的呢如果两个线程同时去抢锁会不会把锁本身也搞乱4.1 前置知识内存中的变量如mutex是被所有线程共享的。CPU 内部的寄存器如%al虽然物理上只有一个但操作系统在进行线程切换时会保存和恢复寄存器的状态。所以从逻辑上讲寄存器是每个线程私有的属于线程上下文。4.2 伪代码分析lock: movb $0, %al xchgb %al, mutex if(al寄存器的内容 0){ return 0; } else 挂起等待; goto lock; unlock: movb $1, mutex 唤醒等待Mutex的线程; return 0;4.2.1lock原理(⭐⭐⭐)movb $0, %al 这行代码把0写入当前线程私有的%al寄存器。xchgb%al, mutex最核心的一句xchgb是汇编指令中的 Exchange交换。它的作用是将%al寄存器里的值和内存中mutex的值进行对调。极其重要这是一条由CPU 硬件保证的原子指令只要这条指令在执行CPU 就会锁住总线绝对不允许其他线程在这个瞬间访问mutex所在的内存。哪怕有多个核同时执行这条指令硬件也会强制它们排队一次只能交换一个。if(al寄存器的内容 0) 交换完毕后线程低头看看自己私有的%al寄存器。此时有两种情况情景 A抢锁成功原本内存mutex里是1交换后内存变成了0而当前线程的%al变成了1。条件 0成立函数return 0成功拿到锁进入临界区情景 B抢锁失败锁已经被别人拿走了内存mutex早就变成了0。交换后内存还是0当前线程的%al换回来的也是0。条件不成立。else { 挂起等待; goto lock; } 如果换回来的是0说明没抢到锁线程就会调用操作系统的接口将自己挂起休眠让出 CPU。等未来被唤醒后再跳转回lock标签处重新尝试交换的过程。4.2.2unlock原理movb $1, mutex 既然线程已经执行完临界区的代码了直接把数字1写回内存的mutex变量中。唤醒等待Mutex的线程; 锁放回去后通知操作系统“我已经完事了你可以把之前因为抢不到锁而挂起休眠的那些线程叫醒了。” 那些被叫醒的线程就会重新去执行xchgb指令争抢刚刚被放回来的1。return 0; 解锁成功退出函数。4.2.3 lock中途发生进程切换即使此时发生了线程切换操作系统会将线程 A 此时的所有寄存器状态包括%al 1以及程序计数器指向if语句完好无损地打包保存到线程 A 的专属内存区域TCB线程控制块中。一段时间后操作系统再次唤醒线程 A把 CPU 交给它。操作系统把之前保存的寄存器数据重新塞回 CPU。此时CPU 的%al寄存器瞬间恢复成了1程序计数器也指回了if语句。5. 互斥量的RAII 封装5.1 传统手动加解锁的缺陷void updateData() { mutex.Lock(); // 进门上锁 if (condition_1) { // 业务逻辑... mutex.Unlock(); // 必须记得解锁 return; } if (condition_2) { // 业务逻辑... // 糟糕这里程序员忘记写 mutex.Unlock() 就直接 return 了 return; } try { // 执行可能抛出异常的代码 doSomethingDangerous(); } catch (...) { mutex.Unlock(); // 异常分支也得记得解锁 throw; } mutex.Unlock(); // 正常走完出门解锁 }维护成本极高只要函数中有return、break、continue你都必须在它们前面补上Unlock()。一旦漏掉一个就会导致锁永远无法释放其他等待这把锁的线程将永久卡死死锁。异常不安全如果在临界区内调用了会抛出异常的函数或者发生了除零错误等程序流会直接跳出当前函数导致底部的Unlock()根本没有机会执行。5.2RAIIResource Acquisition Is Initialization资源获取即初始化#pragma once #include pthread.h // 1. 互斥锁封装 (彻底隐藏 pthread_mutex_t) class Mutex { public: // 构造函数自动初始化底层锁 Mutex() { pthread_mutex_init(lock_, nullptr); } // 析构函数自动销毁底层锁 ~Mutex() { pthread_mutex_destroy(lock_); } void Lock() { pthread_mutex_lock(lock_); } void Unlock() { pthread_mutex_unlock(lock_); } // 防拷贝 Mutex(const Mutex) delete; Mutex operator(const Mutex) delete; private: // 外面的人根本不知道它的存在。 pthread_mutex_t lock_; }; // 2. 锁的生命周期守卫 (负责自动加锁/解锁) class LockGuard { public: LockGuard(Mutex *mutex) : mutex_(mutex) { if (mutex_) { mutex_-Lock(); } } ~LockGuard() { if (mutex_) { mutex_-Unlock(); } } LockGuard(const LockGuard) delete; LockGuard operator(const LockGuard) delete; private: Mutex *mutex_; };构造即加锁当我们在大括号{}内实例化一个LockGuard局部对象时它的构造函数会自动帮我们调用Lock()。析构即解锁C 语言标准提供了一个绝对的保证——无论局部对象是因为什么原因正常执行完毕、提前return、甚至抛出异常离开它的作用域编译器都必定会自动、强行地调用该对象的析构函数。二、可重入函数与线程安全1. 概念1.1 可重入可重入是指一个函数可以被多个执行流包括单线程中的信号处理程序、递归调用、多线程同时调用而无需担心数据损坏。可重入函数不依赖于任何共享数据如全局变量、静态变量也不调用不可重入的函数。1.2 线程安全线程安全是指一个函数、变量或对象在多个线程同时调用时能够正确地处理共享数据不会导致数据竞争、不一致或崩溃。2. 常见情况2.1 常见可重入情况只使用局部变量局部变量分配在栈上每次调用都有独立的副本。不修改全局或静态数据如果必须使用全局数据应使用线程局部存储thread_local但注意信号处理程序可能仍会访问同一线程的数据。不调用不可重入函数如malloc、printf、fopen等标准库函数很多不是可重入的。不持有锁锁本身可能引发死锁且锁的实现可能调用不可重入的系统调用。2.2 常见线程安全情况每个线程对全局变量或者静态变量只有读取的权限而没有写入的权限一般来说这些线程是安全的类或者接口对于线程来说都是原子操作多个线程之间的切换不会导致该接口的执行结果存在二义性3. 联系与区别3.1 联系函数是可重入的那就是线程安全的。函数是不可重入的那就不能由多个线程使用有可能引发线程安全问题。如果一个函数中有全局变量那么这个函数既不是线程安全也不是可重入的。3.2 区别维度线程安全可重入关注点多线程并发访问共享数据任意执行流包括信号处理程序重入实现手段通常使用锁、原子操作等同步机制避免使用共享数据只操作局部变量锁的使用允许使用锁来保护临界区禁止持有锁否则可能在重入时死锁适用场景多线程程序信号处理程序、递归调用、多线程包含关系线程安全的函数不一定是可重入的可重入的函数一定是线程安全的逆命题不成立4. 常见误区(⭐⭐⭐)(1)线程安全就是可重入❌ 错。线程安全只保证多线程并发时的正确性但可能依赖锁而锁在信号处理中会导致死锁。(2)加了锁的函数就是可重入的❌ 错。加锁反而可能破坏可重入性因为同一线程重入时会死锁。(3)可重入函数性能一定比线程安全函数好✅ 对。可重入函数无需加锁避免了锁竞争和上下文切换开销。(4)所有递归函数都必须是可重入的❌ 错。如果递归函数只使用局部变量自然是可重入的但如果使用静态变量则可能出错。三、死锁1. 概念死锁是指在多线程或多进程环境中两个或多个执行流因为争夺资源而造成的一种互相等待的现象。若无外力作用它们都将永远无法向下推进。最经典的场景是线程 A 拿到了锁 1想要获取锁 2同时线程 B 拿到了锁 2想要获取锁 1。双方都在等对方手里的钥匙最终导致程序永久卡死。2. 必要条件死锁的发生并不是随机的。只有当以下四个条件同时成立时死锁才会发生缺一不可(1)互斥条件 (Mutual Exclusion)含义资源是独占的。一把锁在同一时刻只能被一个线程持有。比如我们封装的Mutex天然具备这个属性。(2)请求与保持条件 (Hold and Wait)含义一个线程已经持有了一个资源保持但又提出了新的资源请求请求。而新的资源正被其他线程占用导致当前线程阻塞但它阻塞时绝不释放自己已经持有的资源。(3)不剥夺条件 (No Preemption)含义线程已获得的资源在未使用完之前操作系统或其他线程不能强行把它抢走剥夺只能由持有该资源的线程自己主动释放比如调用Unlock。(4)循环等待条件 (Circular Wait)含义系统中存在一个由多个线程组成的“环形等待链”。比如线程 A 等 BB 等 CC 等 A。首尾相连形成死循环。3. 避免死锁的方式3.1 一次性分配(破坏“请求与保持”)策略要求线程在开始执行前必须一次性申请它所需要的全部锁。如果无法凑齐全部锁就一个都不拿进入等待。缺点编码困难且资源利用率极低有些锁可能要到函数最后才用到但一开始就被占用了。3.2 主动退让(破坏“不剥夺”)策略当一个线程已经持有了锁 A再去请求锁 B 时如果发现锁 B 拿不到被别人占了它不能傻等而是必须立刻释放自己手里的锁 A过一段时间再从头尝试。3.3 按序加锁(破坏“循环等待”)策略这是软件工程中最常见、最有效的防死锁方案。我们人为规定一个全局的加锁顺序。假设系统中有锁 1、锁 2、锁 3所有线程必须严格按照 1 - 2 - 3 的顺序去获取锁绝对不允许倒序或乱序。效果如果线程 A 和 B 都需要锁 1 和锁 2。因为规定了顺序它们都会先去抢锁 1。没抢到锁 1 的线程根本没资格去抢锁 2。这就从根本上切断了“环形等待链”。3.4 银行家算法策略这是一种操作系统的资源分配算法。在每次分配资源如内存、设备前系统都会进行一次数学模拟推演计算这次分配会不会导致系统进入“不安全状态”即潜在的死锁风险。如果会就拒绝分配。四、线程同步1. 竞态条件与同步概念1.1 竞态条件在多线程环境中如果程序的执行结果依赖于操作系统对线程的调度顺序这种现象就叫竞态条件。举个例子有两个线程线程 A 负责向队列里写数据线程 B 负责从队列里读数据。如果调度器一直偏心疯狂给 B 分配时间片B 就会不断地去空队列里“读”每次都读不到每次都在做无用功疯狂加锁、判断为空、解锁。这不仅浪费了大量 CPU 资源还可能因为时序问题引发不可预期的 Bug。1.2 线程同步为了解决竞态条件我们引入了同步。同步的本质是“排兵布阵”它保证多个线程不仅能安全地访问临界资源还能按照一定的顺序或特定条件协同工作。如果说互斥锁是“谁抢到谁执行没抢到的就在门外死等”那么同步机制就是“条件不满足时你先去睡大觉条件满足了我再叫醒你”。这种机制极大地提高了多线程程序的运行效率。2. 条件变量在 Linux 环境下实现线程同步最经典、最常用的就是条件变量Condition Variable。通俗类比假设你去餐厅吃饭消费者线程发现厨师生产者线程还没做好菜。没有条件变量时你每隔一秒钟就跑去厨房问一次“做好了吗”这叫轮询 Polling极其浪费体力/CPU。有条件变量时服务员给你一个取餐器让你去沙发上睡觉等待。等厨师把菜做好了厨师会按铃发送信号你的取餐器响了你被唤醒直接去端菜。条件变量就是操作系统提供给线程的那个“取餐器”。3. 条件变量函数(与互斥量非常相似)在 Linux 系统中条件变量的数据类型是pthread_cond_t。和互斥量一样当定义了这样一个变量时必须对其进行初始化才能被操作系统识别并使用。初始化条件变量同样有两种完全不同的方式静态初始化和动态初始化。3.1 初始化3.1.1 静态初始化如果条件变量被定义为全局变量或者静态static变量可以直接使用系统提供的宏来一键初始化pthread_cond_t cond PTHREAD_COND_INITIALIZER;特点极其简单不需要写初始化函数也不需要后续手动销毁。但它只能使用默认属性且不能用于局部变量或动态分配new/malloc的内存。3.1.2 动态初始化如果条件变量是局部变量、动态申请在堆上的或者被封装在 C 类内部就必须调用系统 API 进行动态初始化int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);cond指向你需要初始化的条件变量对象的指针。attr条件变量的属性设置。通常我们不需要特殊的属性直接传NULL即可表示使用默认属性。返回值成功返回 0失败返回错误码。3.2 条件变量等待当线程发现业务条件不满足例如队列为空、资源未就绪时必须调用此函数让自己进入睡眠挂起状态等待被其他线程唤醒。int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);核心行为机制原子性解锁并休眠该函数内部会自动释放传入的mutex锁并将当前线程挂起加入到该条件变量的等待队列中。这两个动作是原子性的绝不会被其他操作打断。唤醒后重新竞争锁当线程被其他线程发信号唤醒时它并不会立即返回而是会自动去重新竞争并加锁传入的mutex。只有成功拿到锁之后pthread_cond_wait才会真正结束并返回。补充超时等待pthread_cond_timedwait。如果不希望线程“死等”可以使用timedwait。它允许设置一个绝对时间的超时界限。如果时间到了还没有人发信号唤醒它它会自动醒来并返回一个超时错误码通常是ETIMEDOUT让线程可以去执行其他降级逻辑。3.3 条件变量唤醒(Signal / Broadcast)当某个线程改变了共享变量使得某个条件满足了例如往队列里塞入了数据它需要负责叫醒正在等待该条件的线程。int pthread_cond_signal(pthread_cond_t *cond); int pthread_cond_broadcast(pthread_cond_t *cond);核心行为机制单播 (Signal)唤醒在此条件变量上等待的至少一个线程。具体唤醒哪一个取决于操作系统的调度策略通常是优先级最高或等待最久的线程。广播 (Broadcast)唤醒在此条件变量上等待的所有线程。这些被唤醒的线程接下来会去竞争那把关联的互斥锁拿到锁的继续执行没拿到的再次阻塞等锁。3.4 销毁条件变量当条件变量不再使用时必须销毁它释放系统资源。int pthread_cond_destroy(pthread_cond_t *cond);参数传入你要销毁的条件变量指针。返回值成功返回 0失败返回错误码。销毁互斥量的三大重点静态初始化的条件变量使用PTHREAD_COND_INITIALIZER初始化的条件变量不需要也不应该调用pthread_cond_destroy。禁止在等待时销毁绝对不要销毁一个当前还有线程在等待的条件变量否则会导致未定义行为通常返回EBUSY。确保无等待线程在销毁前必须保证所有等待该条件变量的线程都已经退出或不再使用它。4. pthread_cond_wait 中的互斥量(⭐⭐⭐)为什么pthread_cond_wait必须绑定一个互斥量答案在于防止丢失唤醒和操作的原子性。(1)线程在等待条件前必须先检查共享资源如队列是否为空而这个检查必须在加锁下进行否则会出现数据竞争。(2)但检查后发现条件不满足时线程不能带着锁去睡觉否则其他线程永远无法修改条件导致死锁。(3)因此线程需要在挂起前释放锁。pthread_cond_wait的关键之处就在于它将“释放锁”和“挂起线程”这两个动作合并成一个原子操作——既不会在解锁后、挂起前丢失其他线程发来的信号又能保证线程被唤醒后自动重新获取锁然后再次检查条件。正是这种原子性设计使得条件变量能够安全、高效地协调线程间的协作避免竞态条件和信号丢失。错误示例假设存在一个不需要互斥量的cond_wait// 错误代码——仅用于说明问题 if (condition false) { // 如果此时被中断另一个线程改变了condition并signal信号就会丢失 cond_wait(cond); // 假想的无锁等待函数 }5. 条件变量使用规范5.1 等待线程的写法(⭐⭐⭐)pthread_mutex_lock(mutex); // 1. 先加锁 // 2. 必须用 while 循环判断 while (条件不满足) { // 3. 释放锁并睡眠被唤醒后自动重新加锁 pthread_cond_wait(cond, mutex); } // 4. 醒来且跳出 while 循环说明条件真正满足了执行业务逻辑 做你想做的事(比如取出数据); pthread_mutex_unlock(mutex); // 5. 完事后解锁假设用了if。线程被唤醒后直接往下执行。但在多 CPU 架构下有时候因为操作系统的底层原因或者由于被broadcast唤醒了多个线程当你这个线程真正拿到锁并从wait返回时那个让你满足的条件可能已经被其他抢在前面的线程给破坏了比如数据被别人先拿走了。 用while的话醒来第一件事是再次检查条件。如果条件变回不满足了接着进入等待队列。这就是为了防止虚假唤醒。5.2 唤醒线程的写法pthread_mutex_lock(mutex); // 1. 加锁保护共享数据 改变条件(比如往队列里塞入数据); // 2. 生产数据 pthread_cond_signal(cond); // 3. 发信号叫醒等在这个条件变量上的线程 pthread_mutex_unlock(mutex); // 4. 解锁第3步和第4步的顺序在 //某些特定场景下可互换但通常先发信号后解锁或先解锁后发信号都是允许的五、自旋锁1. 概念互斥锁Mutex的逻辑“休眠-唤醒”。如果线程抢不到锁操作系统会把它从 CPU 上剥离放入等待队列休眠。等锁释放了再把它唤醒重新调度到 CPU 上。这个过程会发生上下文切换Context Switch开销很大自旋锁Spinlock的逻辑“忙等待Busy-Waiting”。如果线程抢不到锁它绝对不会去休眠而是死死霸占着 CPU 核心在一个while循环里疯狂地、不断地检查锁有没有被释放。2. 使用场景核心原因在于上下文切换的开销是昂贵的。如果临界区加锁保护的代码段里面只有两三行简单的加减赋值代码执行时间可能只要几纳秒。如果为了这几纳秒去休眠、唤醒线程上下文切换的时间可能高达几微秒“切换的代价”远远大于“等待的代价”。自旋锁的黄金适用场景临界区极短保护的代码非常少绝不能有 I/O 操作如read/write/sleep。多核处理器单核 CPU 用自旋锁是找死因为你在自旋霸占了唯一的 CPU拿锁的那个线程根本没机会运行来释放锁。操作系统内核底层的中断处理中断上下文是不允许休眠的必须用自旋锁。或者像 DPDK、高频交易等极度压榨微秒级延迟的用户态程序。3. POSIX 自旋锁 API 函数自旋锁的接口和 Mutex 几乎一模一样只是把mutex换成了spin。初始化pthread_spin_initint pthread_spin_init(pthread_spinlock_t *lock, int pshared);销毁pthread_spin_destroyint pthread_spin_destroy(pthread_spinlock_t *lock);加锁自旋pthread_spin_lockint pthread_spin_lock(pthread_spinlock_t *lock);解锁pthread_spin_unlockint pthread_spin_unlock(pthread_spinlock_t *lock);4. 易错点绝对禁止在临界区内调用可能导致阻塞的函数如sleep,wait,scanf等。一旦拿到自旋锁的线程睡着了其他所有企图拿锁的线程都会把 CPU 打满到 100%导致系统严重卡顿甚至死机。死锁问题和 Mutex 一样如果线程 A 拿了自旋锁接着又去申请同一个自旋锁会直接导致自己进入死循环。