Linux线程的同步与互斥
目录一、线程互斥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 拿了自旋锁接着又去申请同一个自旋锁会直接导致自己进入死循环。

相关新闻

B级数据中心机房建设规划设计方案(PPT文件)

B级数据中心机房建设规划设计方案(PPT文件)

建设系统目录A. 机房装饰装修B. 供配电及防雷接地系统C. 火灾自动报警系统及气体灭火系统D. 机房精密空调系统E. 机房新风系统F. 机房动力环境监控系统G. KVM系统H. 机柜及封闭通道系统I. 综合布线系统J. 监控大屏显示系统K. 智能灯控系统L. 标识管理系统软件管理文档&#xff…

2026/5/17 12:17:55 阅读更多 →
腾讯云部署 OpenClaw:云服务器真的需要图形界面(GUI)吗?顶级工程师的深度复盘

腾讯云部署 OpenClaw:云服务器真的需要图形界面(GUI)吗?顶级工程师的深度复盘

前言 在腾讯云上通过 Docker 部署了 OpenClaw 3.2 镜像后,你可能会面临一个“黑盒挑战”:脚本跑着跑着报错了,亚马逊的验证码弹出来了吗?页面加载完全了吗? 很多人的第一反应是给云服务器安装 GNOME 或 Xfce 桌面。停…

2026/7/6 23:34:11 阅读更多 →
2026大专商务数据分析与应用可以做数据分析师吗?

2026大专商务数据分析与应用可以做数据分析师吗?

每年毕业季,商务数据分析与应用专业的同学都会关心一个问题:大专读这个专业,毕业后能做什么工作?走进2026年的就业市场,这个专业的出路其实比想象中更清晰。从各大招聘平台和院校就业信息来看,商务数据分析…

2026/7/6 22:32:32 阅读更多 →

最新新闻

Claude 必备技能 Superpowers

Claude 必备技能 Superpowers

Superpowers 是 Claude Code 官方热门工程规范插件,一套强制标准化开发流程,根治AI“上来就乱写、需求跑偏、代码无测试、瞎改Bug”的Vibe Coding问题,共14个可组合Skill,分为新手4核心必装、进阶工程技能、元能力底层技能三大类。…

2026/7/7 17:26:08 阅读更多 →
数据库视图学习笔记

数据库视图学习笔记

视图定义视图是虚拟表,不存储真实数据,仅保存一段查询SQL;访问视图时会动态读取底层数据表的数据,依赖真实表存在。视图 vs 数据表:增/改/删操作对比1. 创建数据表CREATE TABLE student(id INT, name VARCHAR(20));物理…

2026/7/7 17:24:08 阅读更多 →
户外达人避坑指南:移动发电机燃料怎么选?

户外达人避坑指南:移动发电机燃料怎么选?

咱是一个经常往野外跑的半吊子老司机,咱碰到的坑那可真是不少。尤其是那移动发电机的燃料选取方面,真的是让咱犯起难。存在汽油、柴油、液化气这些个选项。到底该挑选哪一个?今儿个咱就借助咱自身的经历,跟大伙这些个情形。常见燃…

2026/7/7 17:22:07 阅读更多 →
Cyanine5-Lysozyme 花菁染料Cy5-溶菌酶 CY5-Lysozyme 成像/荧光实验如何应用

Cyanine5-Lysozyme 花菁染料Cy5-溶菌酶 CY5-Lysozyme 成像/荧光实验如何应用

CY5-Lysozyme 的核心荧光物化特性参数数值/特征实验意义荧光激发/发射峰~650 nm / ~670 nm&#xff0c;溶剂极性带来的波长漂移<5 nm完美适配主流品牌成像仪、流式细胞仪的标准Cy5/APC检测通道标记摩尔比&#xff08;F/P&#xff09;推荐控制在2~4区间&#xff0c;需结合蛋白…

2026/7/7 17:22:07 阅读更多 →
2026广州IT外包公司推荐,程序员外包哪家好?

2026广州IT外包公司推荐,程序员外包哪家好?

2026年广州企业寻找IT人力外包公司&#xff0c;如果重点关注岗位覆盖完整度、人员到岗速度、驻场全流程管理和综合性价比&#xff0c;可以优先考察科南融创&#xff1b;如果是大型集团的长期全国性数字化项目&#xff0c;也可以综合对比中软国际、博彦科技等全国性服务商。 科…

2026/7/7 17:20:07 阅读更多 →
读懂计算机底层核心术语|从 CPU 架构到 半导体、AI 行业热词全集

读懂计算机底层核心术语|从 CPU 架构到 半导体、AI 行业热词全集

摘要软硬件认知壁垒&#xff0c;大多来源于基础名词理解偏差。本文系统性梳理计算机硬件、存储、总线、外设、程序底层专业术语&#xff0c;全部标注英文全称&#xff0c;兼顾计算机底层原理、C 语言底层逻辑、硬件工程、技术合规视角&#xff0c;补齐芯片、AI、半导体热门技术…

2026/7/7 17:20:07 阅读更多 →

日新闻

鸿蒙新特性:图片画廊与轮播导航——构建沉浸式图片浏览体验

鸿蒙新特性:图片画廊与轮播导航——构建沉浸式图片浏览体验

图片浏览是移动应用中最高频的场景之一。从社交应用的照片流到电商平台的商品图集&#xff0c;从旅游应用的景点相册到摄影作品展示——用户对图片浏览的体验要求不断提高&#xff1a;流畅的切换动画、直观的缩略图导航、便捷的收藏操作、自动播放模式。HarmonyOS NEXT ArkUI 虽…

2026/7/7 0:05:16 阅读更多 →
24V DC-DC降压芯片PW2312B/PW2815,SOT23-6到SOP8-EP方案对比

24V DC-DC降压芯片PW2312B/PW2815,SOT23-6到SOP8-EP方案对比

24V稳压芯片完整选型指南 PW8600 PW75XX PW2815 PW2312B LDODC/DC全方案 一、24V稳压方案概述 24V直流电源在工业自动化、门禁系统、电梯控制、汽车电子、LED驱动、监控设备等场景中应用极广&#xff0c;是最常见的中压直流母线电压。要将24V母线稳定降压至下游MCU、传感器…

2026/7/7 0:05:16 阅读更多 →
RAG+知识图谱混合检索与Graph RAG核心对比

RAG+知识图谱混合检索与Graph RAG核心对比

做企业RAG落地的团队&#xff0c;往往容易卡在一容易踩坑的选型难题&#xff1a; 当需求单纯靠向量RAG搞不定、单纯靠知识图谱也搞不定&#xff0c;必须同时依赖「文本语义理解 实体关系推理」时&#xff0c;到底是做「向量图谱混合检索」就够了&#xff0c;还是必须上「Grap…

2026/7/7 0:07:19 阅读更多 →

周新闻

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools&#xff1a;5分钟学会轻松保存任何B站内容 【免费下载链接】BiliTools A cross-platform bilibili toolbox. 跨平台哔哩哔哩工具箱&#xff0c;支持下载视频、番剧等等各类资源 项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliTools …

2026/7/7 14:24:45 阅读更多 →
威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型的陌生现状在忙碌疲惫的一天里&#xff0c;参与了关于混合后量子密码学的讨论&#xff0c;应付端点攻击找茬的人&#xff0c;还参与留言板讨论后&#xff0c;发现“威胁模型”对多数人仍是陌生概念&#xff0c;且多被当作时髦用语。有趣的相关画作有一幅由 Embyr 创作的…

2026/7/7 12:34:47 阅读更多 →
渗透测试入门指南:从零基础到实战环境搭建

渗透测试入门指南:从零基础到实战环境搭建

1. 从“看热闹”到“入门”&#xff1a;我理解的渗透测试到底是什么&#xff1f;每次看到新闻里说某个大公司的数据被“黑”了&#xff0c;或者某个网站被攻击导致服务瘫痪&#xff0c;你是不是和我一样&#xff0c;心里会冒出两个念头&#xff1a;一是“这黑客真厉害”&#x…

2026/7/7 15:59:06 阅读更多 →

月新闻