【Linux系统编程】(四十四)线程同步下篇:条件变量深度解析与 POSIX 信号量实战
目录前言一、灵魂拷问pthread_cond_wait 为何必须绑定互斥量1.1 条件变量的核心作用线程间的状态通知1.2 无互斥量的致命问题错过信号与永久阻塞1.3 互斥量的核心价值让 “解锁 等待” 成为原子操作1.4 再挖一层条件判断为何必须保护二、避坑指南条件变量的标准化使用规范2.1 等待条件的规范写法while 判断而非 if 判断错误写法if 判断条件正确写法while 循环判断条件2.2 核心原因处理 “伪唤醒” 问题2.3 等待条件的完整标准化模板2.4 发送信号的标准化模板2.5 信号选择signal 还是 broadcast三、优雅封装C 实现可复用的条件变量类3.1 前置准备已封装的互斥量类 Lock.hpp3.2 条件变量封装Cond.hpp3.3 封装的核心亮点解析3.4 封装后的使用示例四、另一种同步方案POSIX 信号量详解与实战4.1 POSIX 信号量的核心概念4.2 POSIX 信号量的核心 API4.2.1 初始化信号量sem_init4.2.2 销毁信号量sem_destroy4.2.3 P 操作等待sem_wai4.2.4 V 操作发布sem_post4.3 C 封装 POSIX 信号量简洁易用的 Sem 类4.4 实战基于 POSIX 信号量实现环形队列的生产者消费者模型4.4.1 环形队列封装RingQueue.hpp4.4.2 生产者消费者测试代码ringqueue_test.cpp4.4.3 编译运行与结果分析五、条件变量与 POSIX 信号量的对比与选型建议5.1 核心对比5.2 选型建议总结哈喽各位 C/C 开发者小伙伴们上篇我们聊了线程互斥和条件变量的基础使用相信大家已经对线程同步有了初步的认知。但实际开发中光会用 API 远远不够为什么 pthread_cond_wait 必须搭配互斥量条件变量的使用有哪些避坑规范如何优雅封装条件变量POSIX 信号量又该如何实现高效的线程同步这些都是大家在实际开发中一定会遇到的核心问题。今天这篇线程同步下篇就带大家把这些问题彻底吃透从原理层面刨根问底再到实战层面封装实现最后结合 POSIX 信号量实现环形队列的生产消费模型全程干货满满建议收藏反复研读下面就让我们正式开始吧一、灵魂拷问pthread_cond_wait 为何必须绑定互斥量用过条件变量的小伙伴都知道pthread_cond_wait的函数声明是int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);第二个参数强制要求传入互斥量。很多人一开始只是机械使用却不知道背后的底层逻辑这就很容易写出 bug 代码。今天我们就把这个问题讲透从线程同步的本质和竞态条件的规避两个角度分析。1.1 条件变量的核心作用线程间的状态通知条件变量是实现线程同步的核心机制之一同步的本质是在保证数据安全的前提下让线程按特定顺序访问临界资源。简单来说条件变量解决的是“线程等待某个条件满足直到其他线程通知它条件成立”的问题。比如生产者消费者模型中消费者线程发现队列为空时需要等待生产者生产数据生产者生产完数据后需要通知消费者来消费。这里的“队列非空”就是条件条件变量就是实现这种 “等待 - 通知” 机制的载体。但问题来了条件的判断和等待操作并不是原子的而条件的变化必然牵扯到共享资源比如队列的元素个数的修改这就需要互斥量来保护共享资源。1.2 无互斥量的致命问题错过信号与永久阻塞我们不妨大胆假设一下如果pthread_cond_wait不绑定互斥量我们会怎么写代码大概率会是这样的错误写法// 错误示范无互斥量的条件等待 pthread_mutex_lock(mutex); while (queue.empty()) { // 判断条件队列为空 pthread_mutex_unlock(mutex); // 解锁后等待 pthread_cond_wait(cond, NULL); // 假设可以不传互斥量 pthread_mutex_lock(mutex); // 被唤醒后重新加锁 } // 处理队列数据 pthread_mutex_unlock(mutex);看似合理的代码却隐藏着致命的竞态条件解锁操作和等待操作之间存在时间窗口。具体来说当消费者线程执行完pthread_mutex_unlock(mutex)后还没来得及执行pthread_cond_wait(cond, NULL)时操作系统发生线程调度切换到生产者线程。生产者线程生产了数据调用pthread_cond_signal(cond)发送通知信号。但此时消费者线程还没有进入等待状态这个信号就被永久错过了。当消费者线程再次被调度执行pthread_cond_wait时会一直等待永远不会到来的信号最终导致线程永久阻塞这是多线程开发中典型的 “信号丢失” 问题。1.3 互斥量的核心价值让 “解锁 等待” 成为原子操作pthread_cond_wait设计时强制绑定互斥量本质是为了将 “释放互斥量” 和 “进入等待状态” 封装成一个原子操作从根本上规避上述的竞态条件。当调用pthread_cond_wait(cond, mutex)时函数内部会自动完成三个核心操作原子执行释放传入的互斥量mutex将当前线程挂起加入条件变量cond的等待队列当线程被pthread_cond_signal/broadcast唤醒后自动重新竞争互斥量竞争成功后函数才会返回。这个原子操作的过程保证了线程要么持有互斥量并判断条件要么处于等待状态并释放互斥量不会出现 “解锁后、等待前” 的时间窗口彻底避免了信号丢失的问题。1.4 再挖一层条件判断为何必须保护除了避免信号丢失互斥量的另一个核心作用是保护条件判断的共享资源。条件变量的等待条件本质是对共享资源状态的判断比如队列是否为空、计数器是否为 0。这些共享资源是多个线程共同操作的必须通过互斥量保证访问的原子性。如果没有互斥量保护多个线程同时判断和修改共享资源会导致条件判断的结果失真。比如多个消费者线程同时判断queue.empty()如果没有互斥量可能出现多个线程都判断队列为空并进入等待而生产者只生产了一个数据却唤醒了多个线程导致后续的队列操作出现越界等问题。总结一下pthread_cond_wait 绑定互斥量既是为了让 “解锁 等待” 原子化避免信号丢失也是为了保护条件判断的共享资源保证条件判断的准确性。这两个原因决定了互斥量是条件变量的 “标配”缺一不可。二、避坑指南条件变量的标准化使用规范理解了pthread_cond_wait与互斥量的绑定关系后接下来的关键是如何正确使用条件变量。实际开发中很多小伙伴因为使用不规范写出了存在伪唤醒、条件判断失效的 bug 代码。接下来就给大家总结条件变量的标准化使用规范记住这些规则能避开 99% 的坑2.1 等待条件的规范写法while 判断而非 if 判断这是条件变量使用中最重要、最容易踩坑的点判断等待条件时必须使用 while 循环而不能使用 if 语句。错误写法if 判断条件// 错误if判断存在伪唤醒风险 pthread_mutex_lock(mutex); if (queue.empty()) { // 单次判断 pthread_cond_wait(cond, mutex); } // 处理数据 pthread_mutex_unlock(mutex);正确写法while 循环判断条件// 正确while循环重新判断条件 pthread_mutex_lock(mutex); while (queue.empty()) { // 循环判断 pthread_cond_wait(cond, mutex); } // 处理数据 pthread_mutex_unlock(mutex);2.2 核心原因处理 “伪唤醒” 问题所谓伪唤醒Spurious Wakeup是指线程在没有被pthread_cond_signal/broadcast显式唤醒的情况下从pthread_cond_wait中返回。这种情况在 Linux 等系统中是允许的原因可能是操作系统的调度策略、信号中断等。如果使用if单次判断线程被伪唤醒后会直接跳过条件判断执行后续的临界区代码。但此时条件其实并没有满足比如队列还是空的直接操作共享资源会导致数据越界、逻辑错误等严重问题。而使用while循环判断线程被唤醒后无论是显式唤醒还是伪唤醒会重新检查条件是否满足如果条件不满足会再次调用pthread_cond_wait进入等待状态只有条件满足时才会退出循环执行后续代码。这就从根本上解决了伪唤醒的问题。2.3 等待条件的完整标准化模板结合互斥量的使用和 while 循环判断等待条件的标准化代码模板如下大家可以直接套用// 等待条件的标准模板 pthread_mutex_lock(mutex); // 1. 加锁保护共享资源 while (条件为假) { // 2. 循环判断条件处理伪唤醒 pthread_cond_wait(cond, mutex); // 3. 原子解锁等待 } // 4. 条件为真操作临界资源 // ... 处理共享资源的代码 ... pthread_mutex_unlock(mutex); // 5. 解锁这个模板的核心是“加锁 - 循环判断 - 等待 - 操作 - 解锁”环环相扣缺一不可。2.4 发送信号的标准化模板除了等待条件发送信号pthread_cond_signal/broadcast的使用也有规范核心原则是修改共享资源后在互斥量的保护下发送信号。发送信号的标准化模板// 发送信号的标准模板 pthread_mutex_lock(mutex); // 1. 加锁保护共享资源 // 2. 修改共享资源使等待条件变为真 // ... 修改共享资源的代码 ... pthread_cond_signal(cond); // 3. 发送信号唤醒等待线程 // 也可以用pthread_cond_broadcast(cond)唤醒所有等待线程 pthread_mutex_unlock(mutex); // 4. 解锁为什么要在互斥量保护下发送信号原因是保证共享资源的修改和信号的发送是有序的。如果先解锁再发送信号可能出现解锁后其他线程抢先修改共享资源导致信号发送的时机滞后进而出现线程唤醒后条件又变为假的情况。在互斥量保护下发送信号能保证线程被唤醒时共享资源的状态已经被正确修改。2.5 信号选择signal 还是 broadcast实际开发中我们需要根据场景选择pthread_cond_signal唤醒一个等待线程还是pthread_cond_broadcast唤醒所有等待线程pthread_cond_signal适用于只有一个线程的条件会被满足的场景比如单生产者单消费者模型。优点是开销小不会唤醒多余的线程pthread_cond_broadcast适用于多个线程的条件可能被满足的场景比如多生产者多消费者模型、线程池中的任务通知。缺点是可能唤醒多余的线程但通过 while 循环判断条件多余的线程会重新进入等待不影响逻辑正确性。总结如果不确定用哪种优先使用pthread_cond_broadcast虽然开销稍大但能保证逻辑的正确性避免因漏唤醒导致的线程阻塞问题。三、优雅封装C 实现可复用的条件变量类在 C 开发中我们不会一直直接使用 POSIX 的 C 语言 API而是会将互斥量和条件变量进行面向对象的封装实现可复用、易维护的工具类。这样做的好处是避免手动管理锁的创建和销毁减少内存泄漏利用 RAII 机制自动加锁解锁避免忘记解锁导致的死锁封装后接口更简洁降低使用成本。接下来我们就基于之前封装的互斥量类实现一个通用、安全、可复用的条件变量类全程使用 C 实现兼容 C11 及以上版本。3.1 前置准备已封装的互斥量类 Lock.hpp首先我们需要一个已经封装好的互斥量类包含锁的创建、销毁、加锁、解锁功能并且禁用拷贝和赋值避免多个对象操作同一个锁。这里使用我们之前封装的Mutex类和 RAII 风格的LockGuard类代码如下// Lock.hpp 互斥量封装 #pragma once #include iostream #include pthread.h namespace LockModule { // 互斥量基础类 class Mutex { public: // 禁用拷贝和赋值 Mutex(const Mutex ) delete; const Mutex operator(const Mutex ) delete; // 构造函数初始化互斥量 Mutex() { int ret pthread_mutex_init(_mutex, nullptr); if (ret ! 0) { std::cerr pthread_mutex_init error! std::endl; exit(EXIT_FAILURE); } } // 加锁 void Lock() { int ret pthread_mutex_lock(_mutex); if (ret ! 0) { std::cerr pthread_mutex_lock error! std::endl; exit(EXIT_FAILURE); } } // 解锁 void Unlock() { int ret pthread_mutex_unlock(_mutex); if (ret ! 0) { std::cerr pthread_mutex_unlock error! std::endl; exit(EXIT_FAILURE); } } // 获取原生互斥量指针供条件变量使用 pthread_mutex_t *GetMutexOriginal() { return _mutex; } // 析构函数销毁互斥量 ~Mutex() { pthread_mutex_destroy(_mutex); } private: pthread_mutex_t _mutex; // 原生POSIX互斥量 }; // RAII风格的锁守卫自动加锁解锁 class LockGuard { public: // 构造函数加锁 explicit LockGuard(Mutex mutex) : _mutex(mutex) { _mutex.Lock(); } // 析构函数解锁 ~LockGuard() { _mutex.Unlock(); } // 禁用拷贝和赋值 LockGuard(const LockGuard ) delete; const LockGuard operator(const LockGuard ) delete; private: Mutex _mutex; // 引用互斥量避免拷贝 }; }这个互斥量封装的核心点构造函数初始化互斥量析构函数销毁互斥量实现资源的自动管理禁用拷贝和赋值避免多个Mutex对象操作同一个底层互斥量LockGuard利用 RAII 机制构造加锁、析构解锁彻底避免忘记解锁的问题提供GetMutexOriginal方法返回原生互斥量指针供条件变量绑定使用。3.2 条件变量封装Cond.hpp基于上面的Mutex类我们封装条件变量类Cond核心要求封装 POSIX 条件变量的创建、销毁、等待、唤醒接口等待接口需要接收Mutex对象实现与互斥量的绑定提供Wait等待、Notify唤醒一个、NotifyAll唤醒所有三个核心接口禁用拷贝和赋值保证对象的唯一性。完整的Cond.hpp代码如下// Cond.hpp 条件变量封装 #pragma once #include iostream #include pthread.h #include Lock.hpp // 引入封装的互斥量类 namespace CondModule { // 引入互斥量的命名空间 using namespace LockModule; // 条件变量类 class Cond { public: // 禁用拷贝和赋值 Cond(const Cond ) delete; const Cond operator(const Cond ) delete; // 构造函数初始化条件变量 Cond() { int ret pthread_cond_init(_cond, nullptr); if (ret ! 0) { std::cerr pthread_cond_init error! std::endl; exit(EXIT_FAILURE); } } // 等待接口绑定互斥量原子解锁等待 void Wait(Mutex mutex) { // 传入原生互斥量指针 int ret pthread_cond_wait(_cond, mutex.GetMutexOriginal()); if (ret ! 0) { std::cerr pthread_cond_wait error! std::endl; exit(EXIT_FAILURE); } } // 唤醒一个等待线程 void Notify() { int ret pthread_cond_signal(_cond); if (ret ! 0) { std::cerr pthread_cond_signal error! std::endl; exit(EXIT_FAILURE); } } // 唤醒所有等待线程 void NotifyAll() { int ret pthread_cond_broadcast(_cond); if (ret ! 0) { std::cerr pthread_cond_broadcast error! std::endl; exit(EXIT_FAILURE); } } // 析构函数销毁条件变量 ~Cond() { pthread_cond_destroy(_cond); } private: pthread_cond_t _cond; // 原生POSIX条件变量 }; }3.3 封装的核心亮点解析解耦设计Cond类内部不持有Mutex对象而是通过Wait接口的参数接收Mutex对象。这样做的好处是一个条件变量可以绑定多个互斥量也可以一个互斥量绑定多个条件变量提高了类的通用性避免了代码耦合。异常处理对 POSIX API 的返回值进行判断若调用失败则直接退出程序避免后续的错误传播。实际开发中也可以根据需求修改为抛异常适配不同的错误处理策略。接口简洁对外只暴露Wait、Notify、NotifyAll三个核心接口隐藏了底层的 POSIX API 细节使用者无需关心底层实现降低了使用成本。资源自动管理构造函数初始化条件变量析构函数销毁条件变量实现了RAII 风格的资源管理避免了手动管理导致的资源泄漏。3.4 封装后的使用示例封装后的条件变量使用起来非常简洁结合Mutex和LockGuard实现一个简单的 “线程等待 - 通知” 示例代码如下// cond_test.cpp 封装后的条件变量使用示例 #include iostream #include unistd.h #include pthread.h #include Lock.hpp #include Cond.hpp // 命名空间 using namespace LockModule; using namespace CondModule; // 全局的互斥量和条件变量 Mutex g_mutex; Cond g_cond; // 线程函数等待条件变量通知 void *thread_func(void *arg) { std::string thread_name (char *)arg; while (true) { LockGuard lock(g_mutex); // RAII自动加锁 // 循环等待条件这里简化为永久等待实际开发中替换为业务条件 g_cond.Wait(g_mutex); // 被唤醒后执行业务逻辑 std::cout thread_name 被唤醒执行业务逻辑... std::endl; } return nullptr; } int main() { // 创建两个线程 pthread_t t1, t2; pthread_create(t1, nullptr, thread_func, (void *)Thread-1); pthread_create(t2, nullptr, thread_func, (void *)Thread-2); // 主线程每隔1秒发送一次通知 int cnt 5; while (cnt--) { sleep(1); LockGuard lock(g_mutex); // 加锁保护 std::cout 主线程发送通知剩余次数 cnt std::endl; g_cond.NotifyAll(); // 唤醒所有等待线程 } // 等待线程退出 pthread_join(t1, nullptr); pthread_join(t2, nullptr); return 0; }编译运行# 编译链接pthread库 g cond_test.cpp -o cond_test -lpthread # 运行 ./cond_test运行结果主线程发送通知剩余次数4 Thread-1 被唤醒执行业务逻辑... Thread-2 被唤醒执行业务逻辑... 主线程发送通知剩余次数3 Thread-1 被唤醒执行业务逻辑... Thread-2 被唤醒执行业务逻辑... 主线程发送通知剩余次数2 Thread-1 被唤醒执行业务逻辑... Thread-2 被唤醒执行业务逻辑... 主线程发送通知剩余次数1 Thread-1 被唤醒执行业务逻辑... Thread-2 被唤醒执行业务逻辑... 主线程发送通知剩余次数0 Thread-1 被唤醒执行业务逻辑... Thread-2 被唤醒执行业务逻辑...可以看到封装后的条件变量使用非常简洁结合LockGuard实现了自动加锁解锁彻底避免了手动管理锁的繁琐和错误。四、另一种同步方案POSIX 信号量详解与实战除了条件变量POSIX 还提供了信号量Semaphore作为线程同步的机制。信号量本质是一个计数器通过对计数器的原子操作实现对共享资源的访问控制。相比条件变量信号量的使用更简洁尤其适合生产者消费者模型、有限资源的访问控制等场景。4.1 POSIX 信号量的核心概念POSIX 信号量分为无名信号量和有名信号量我们这里主要讲解无名信号量适用于线程间同步核心概念计数器信号量的核心是一个非负整数计数器代表可用的共享资源数量P 操作等待sem_wait将计数器减 1。如果计数器减 1 后小于 0当前线程会被阻塞直到其他线程执行 V 操作V 操作发布sem_post将计数器加 1。如果计数器加 1 后大于等于 0会唤醒一个阻塞在该信号量上的线程原子性P 操作和 V 操作都是原子操作由操作系统保证无需额外的互斥量保护底层已实现。信号量的同步逻辑非常简单当共享资源可用时计数器大于 0线程可以执行 P 操作获取资源当共享资源不可用时计数器为 0线程执行 P 操作会被阻塞直到其他线程执行 V 操作释放资源。4.2 POSIX 信号量的核心 APIPOSIX 信号量的头文件是semaphore.h核心 API 包括初始化、销毁、P 操作、V 操作所有 API 的返回值都是成功返回 0失败返回 - 1 并设置errno。4.2.1 初始化信号量sem_initint sem_init(sem_t *sem, int pshared, unsigned int value);参数说明sem指向要初始化的信号量对象的指针pshared共享属性0 表示线程间共享非 0 表示进程间共享本篇只关注线程间同步设为 0 即可value信号量的初始计数器值代表初始的可用资源数量。4.2.2 销毁信号量sem_destroyint sem_destroy(sem_t *sem);功能销毁已初始化的信号量释放相关资源。注意不能销毁一个正在被线程等待的信号量。4.2.3 P 操作等待sem_waiint sem_wait(sem_t *sem);功能原子地将信号量计数器减 1。如果计数器减 1 后小于 0当前线程阻塞直到其他线程执行sem_post。4.2.4 V 操作发布sem_postint sem_post(sem_t *sem);功能原子地将信号量计数器加 1。如果计数器加 1 后大于等于 0唤醒一个阻塞在该信号量上的线程。4.3 C 封装 POSIX 信号量简洁易用的 Sem 类和互斥量、条件变量一样我们对 POSIX 信号量进行 C 面向对象封装实现资源自动管理和接口简洁化代码如下// Sem.hpp POSIX信号量封装 #pragma once #include iostream #include semaphore.h #include cstdlib class Sem { public: // 禁用拷贝和赋值 Sem(const Sem ) delete; const Sem operator(const Sem ) delete; // 构造函数初始化信号量指定初始计数器值 explicit Sem(unsigned int value 0) { int ret sem_init(_sem, 0, value); if (ret ! 0) { std::cerr sem_init error! std::endl; exit(EXIT_FAILURE); } } // P操作等待计数器减1 void P() { int ret sem_wait(_sem); if (ret ! 0) { std::cerr sem_wait error! std::endl; exit(EXIT_FAILURE); } } // V操作发布计数器加1 void V() { int ret sem_post(_sem); if (ret ! 0) { std::cerr sem_post error! std::endl; exit(EXIT_FAILURE); } } // 析构函数销毁信号量 ~Sem() { sem_destroy(_sem); } private: sem_t _sem; // 原生POSIX信号量 };这个封装的核心亮点和之前的互斥量、条件变量一致RAII 资源管理、禁用拷贝赋值、接口简洁、异常处理使用者只需调用P()和V()即可实现信号量的等待和发布。4.4 实战基于 POSIX 信号量实现环形队列的生产者消费者模型信号量最经典的应用场景就是生产者消费者模型相比条件变量实现的阻塞队列信号量实现的环形队列更高效、更简洁。接下来我们就实现一个多生产者多消费者的环形队列模型核心设计环形队列用std::vector模拟固定容量通过下标取模实现环形特性两个信号量空间信号量_room_sem计数器为环形队列的空闲空间数初始值为队列容量生产者关注数据信号量_data_sem计数器为环形队列的有效数据数初始值为 0消费者关注两个互斥量生产者互斥量_productor_mutex保证多个生产者的入队操作互斥消费者互斥量_consumer_mutex保证多个消费者的出队操作互斥下标指针_productor_step生产者入队下标、_consumer_step消费者出队下标通过取模实现环形移动。4.4.1 环形队列封装RingQueue.hpp// RingQueue.hpp 基于信号量的环形队列 #pragma once #include iostream #include vector #include pthread.h #include Sem.hpp // 模板类支持任意类型的任务 template typename T class RingQueue { public: // 构造函数初始化环形队列指定容量 explicit RingQueue(int cap) : _cap(cap), _ring_queue(cap), _room_sem(cap), // 空间信号量初始值为容量 _data_sem(0), // 数据信号量初始值为0 _productor_step(0), _consumer_step(0) { // 初始化互斥量 pthread_mutex_init(_productor_mutex, nullptr); pthread_mutex_init(_consumer_mutex, nullptr); } // 入队操作生产者调用 void Enqueue(const T in) { // 1. P操作获取空闲空间无空间则阻塞 _room_sem.P(); // 2. 生产者互斥保证入队操作原子性 pthread_mutex_lock(_productor_mutex); // 3. 入队下标取模实现环形 _ring_queue[_productor_step] in; _productor_step % _cap; // 4. 解锁 pthread_mutex_unlock(_productor_mutex); // 5. V操作发布数据通知消费者 _data_sem.V(); } // 出队操作消费者调用 void Pop(T *out) { // 1. P操作获取有效数据无数据则阻塞 _data_sem.P(); // 2. 消费者互斥保证出队操作原子性 pthread_mutex_lock(_consumer_mutex); // 3. 出队下标取模实现环形 *out _ring_queue[_consumer_step]; _consumer_step % _cap; // 4. 解锁 pthread_mutex_unlock(_consumer_mutex); // 5. V操作发布空间通知生产者 _room_sem.V(); } // 析构函数销毁互斥量 ~RingQueue() { pthread_mutex_destroy(_productor_mutex); pthread_mutex_destroy(_consumer_mutex); } // 禁用拷贝和赋值 RingQueue(const RingQueue ) delete; const RingQueue operator(const RingQueue ) delete; private: std::vectorT _ring_queue; // 环形队列的底层容器 int _cap; // 环形队列的容量 Sem _room_sem; // 空间信号量生产者关注 Sem _data_sem; // 数据信号量消费者关注 int _productor_step; // 生产者入队下标 int _consumer_step; // 消费者出队下标 pthread_mutex_t _productor_mutex; // 生产者互斥锁 pthread_mutex_t _consumer_mutex; // 消费者互斥锁 };4.4.2 生产者消费者测试代码ringqueue_test.cpp// ringqueue_test.cpp 环形队列的生产者消费者测试 #include iostream #include unistd.h #include pthread.h #include random #include RingQueue.hpp // 定义环形队列容量 const int QUEUE_CAP 5; // 全局环形队列对象存储int类型数据 RingQueueint g_rq(QUEUE_CAP); // 生产者线程函数不断生产随机数入队 void *productor_func(void *arg) { std::string productor_name (char *)arg; // 随机数生成器 std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution dis(1, 100); while (true) { int data dis(gen); g_rq.Enqueue(data); std::cout productor_name 生产数据 data std::endl; // 随机休眠模拟生产耗时 usleep(dis(gen) * 1000); } return nullptr; } // 消费者线程函数不断出队并打印数据 void *consumer_func(void *arg) { std::string consumer_name (char *)arg; // 随机数生成器 std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution dis(1, 100); while (true) { int data; g_rq.Pop(data); std::cout consumer_name 消费数据 data std::endl; // 随机休眠模拟消费耗时 usleep(dis(gen) * 1000); } return nullptr; } int main() { // 创建2个生产者线程3个消费者线程 pthread_t p1, p2, c1, c2, c3; pthread_create(p1, nullptr, productor_func, (void *)Productor-1); pthread_create(p2, nullptr, productor_func, (void *)Productor-2); pthread_create(c1, nullptr, consumer_func, (void *)Consumer-1); pthread_create(c2, nullptr, consumer_func, (void *)Consumer-2); pthread_create(c3, nullptr, consumer_func, (void *)Consumer-3); // 等待线程退出实际会一直运行 pthread_join(p1, nullptr); pthread_join(p2, nullptr); pthread_join(c1, nullptr); pthread_join(c2, nullptr); pthread_join(c3, nullptr); return 0; }4.4.3 编译运行与结果分析编译g ringqueue_test.cpp -o ringqueue_test -lpthread -stdc11运行./ringqueue_test运行结果节选Productor-1 生产数据45 Productor-2 生产数据78 Consumer-1 消费数据45 Consumer-2 消费数据78 Productor-1 生产数据23 Productor-2 生产数据91 Consumer-3 消费数据23 Consumer-1 消费数据91 Productor-1 生产数据56 Productor-2 生产数据88 Consumer-2 消费数据56 Consumer-3 消费数据88结果分析生产者生产数据后消费者能及时消费实现了同步多个生产者和多个消费者同时运行无数据错乱实现了互斥当环形队列满时生产者会被阻塞当队列空时消费者会被阻塞实现了阻塞等待。整个模型的核心是两个信号量的协同工作空间信号量控制生产者的生产速度数据信号量控制消费者的消费速度互斥量保证多个生产者 / 消费者的操作互斥最终实现了高效、安全的生产者消费者模型。五、条件变量与 POSIX 信号量的对比与选型建议学到这里大家肯定会有疑问条件变量和 POSIX 信号量都是线程同步机制该如何选择接下来我们从底层实现、使用场景、优缺点三个维度进行对比给出明确的选型建议。5.1 核心对比特性条件变量POSIX 信号量无名核心原理基于 “等待 - 通知” 机制依赖互斥量保护共享资源基于计数器的原子操作P/V 操作实现阻塞和唤醒资源依赖必须绑定互斥量使用无需额外互斥量底层已实现原子性条件判断需手动判断共享资源状态必须用 while 循环处理伪唤醒无需手动判断计数器直接表示资源状态无伪唤醒使用复杂度较高需关注互斥量、条件判断、伪唤醒较低只需调用 P/V 操作逻辑简单灵活性高可实现复杂的同步逻辑如多条件等待较低主要适用于简单的资源计数和同步适用场景复杂的线程同步场景如线程池、多条件的生产者消费者模型简单的资源访问控制、固定容量的生产者消费者模型、有限资源的并发访问5.2 选型建议优先使用 POSIX 信号量如果你的场景是有限资源的访问控制、固定容量的生产者消费者模型如环形队列建议优先使用 POSIX 信号量因为其使用更简洁无需处理伪唤醒开发效率更高bug 率更低。使用条件变量如果你的场景是复杂的线程同步比如多条件等待一个线程需要等待多个条件满足、线程池的任务通知、自定义的阻塞队列建议使用条件变量因为其灵活性更高能实现更复杂的同步逻辑。混合使用在一些复杂的系统中也可以将条件变量和信号量混合使用比如用信号量控制资源数量用条件变量实现更精细的状态通知。总结线程同步是多线程开发的核心和难点也是面试的高频考点。希望大家通过本篇的学习不仅能知其然更能知其所以然从原理层面理解同步机制从实战层面掌握封装和使用技巧。后续我会继续更新多线程开发的进阶内容关注我不迷路最后大家如果有任何问题欢迎在评论区留言讨论一起学习一起进步

相关新闻

帝国CMS处理Word截图粘贴发布的技巧?

帝国CMS处理Word截图粘贴发布的技巧?

CMS编辑器高级文档导入功能开发日志 1. 需求分析与技术调研 作为广东PHP开发团队的成员,我最近接手了一个企业CMS官网项目的升级任务。客户需要增强新闻发布功能,特别要求支持多种文档格式的直接导入和粘贴功能。 1.1 核心需求梳理 文档格式支持&…

2026/7/4 8:40:16 阅读更多 →
前端如何实现帝国CMS的Word文档一键发布?

前端如何实现帝国CMS的Word文档一键发布?

要求:开源,免费,技术支持 CMS:帝国CMS(EmpireCMS) 版本:EmpireCMS_7.5_SC_UTF8 编辑器:UEditor1.4x 功能:导入Word,导入Excel,导入PPT(PowerPoint),导入PDF,复制粘贴word…

2026/7/4 8:57:08 阅读更多 →
国产化CMS怎样兼容帝国CMS的Word导入功能?

国产化CMS怎样兼容帝国CMS的Word导入功能?

CMS企业官网Word文档导入功能开发记录 需求分析 作为浙江的一名PHP开发者,我最近接手了一个帝国CMS企业官网的外包项目。客户提出了一个关键需求:在后台新闻管理系统的文章发布模块中,增加Word/Excel/PPT/PDF文档导入和一键粘贴功能。这个需…

2026/7/5 2:24:53 阅读更多 →

最新新闻

5个步骤搭建免费动作捕捉系统:FreeMoCap完全指南

5个步骤搭建免费动作捕捉系统:FreeMoCap完全指南

5个步骤搭建免费动作捕捉系统:FreeMoCap完全指南 【免费下载链接】freemocap Free Motion Capture for Everyone 💀✨ 项目地址: https://gitcode.com/GitHub_Trending/fr/freemocap FreeMoCap是一个免费开源的动作捕捉系统,为所有人提…

2026/7/5 4:17:14 阅读更多 →
Day3 第二章 链表part2

Day3 第二章 链表part2

了解链表 1. 什么是链表 链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)…

2026/7/5 4:17:14 阅读更多 →
聊城食品洁净车间建设指南,按加工场景适配净化板更耐用

聊城食品洁净车间建设指南,按加工场景适配净化板更耐用

聊城作为鲁西农副产品加工核心区域,形成禽肉屠宰、速冻预制菜、果蔬深加工、杂粮面点、宠物食品五大加工集群,大量新建洁净车间、老旧厂房改造需求持续增多。本地的特殊工况,也让选择板材变得复杂纠结起来。 生产线全天用水冲洗,血…

2026/7/5 4:15:13 阅读更多 →
基于TB9051FTG与MSP432的静音直流电机控制方案

基于TB9051FTG与MSP432的静音直流电机控制方案

1. 项目背景与核心需求在工业自动化、消费电子和机器人领域,直流电机控制一直是个经典课题。传统PWM调速方案虽然简单易实现,但存在明显的电磁噪声和机械振动问题——当PWM频率落在人耳可听范围(20Hz-20kHz)时,电机会发…

2026/7/5 4:13:13 阅读更多 →
Power BI热力图实战:用矩阵+条件格式驱动业务决策

Power BI热力图实战:用矩阵+条件格式驱动业务决策

1. 为什么一张“彩色表格”能成为业务决策的加速器?在Power BI里做可视化,很多人第一反应是柱状图、折线图、饼图——稳妥、熟悉、老板一眼能看懂。但真正让我在客户现场被反复追问“这个怎么做的?”“能不能再加一列?”“能不能按…

2026/7/5 4:11:12 阅读更多 →
轻量级AI智能体:安全、场景与硬件穿透的工程实践

轻量级AI智能体:安全、场景与硬件穿透的工程实践

1. 项目概述:轻量级AI智能体不是“减配版”,而是精准适配的生产力工具最近在技术圈和办公软件社群里,“养龙虾”这个词火了——它不是水产养殖指南,而是对 OpenClaw 架构下各类 AI 智能体(Agent)产品的戏称…

2026/7/5 4:11:12 阅读更多 →

日新闻

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

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

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

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

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

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

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

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

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

2026/7/5 0:07:38 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/7/5 0:07:38 阅读更多 →

月新闻