【Linux系统】线程同步

【Linux系统】线程同步 1. 条件变量根据这个问题我们可以引入条件变量Condition Variable。条件变量允许线程在特定条件不满足时主动等待而不是忙等待或不公平地竞争锁。为什么会出现只有一个线程工作的情况在输出中只有thread 1在处理所有售票这是因为锁竞争的不公平性当一个线程释放锁后它可能立即又重新获取锁而其他线程没有机会获取调度策略操作系统的线程调度可能优先调度刚刚释放锁的线程没有等待机制线程在无法获取票时没有等待而是继续竞争锁1.1 条件变量的基本概念条件变量是一种同步机制允许线程在某个条件不满足时挂起等待直到其他线程改变条件并通知它。条件变量的主要操作等待pthread_cond_wait(cond, mutex)原子性地释放互斥锁并进入等待状态被唤醒后重新获取互斥锁信号pthread_cond_signal(cond)唤醒一个等待该条件变量的线程广播pthread_cond_broadcast(cond)唤醒所有等待该条件变量的线程1.2 条件变量函数初始化条件变量代码语言javascriptAI代码解释int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);参数说明cond指向要初始化的条件变量的指针attr条件变量属性通常为NULL表示使用默认属性返回值成功返回0失败返回错误码使用方式代码语言javascriptAI代码解释// 动态初始化 pthread_cond_t cond; if (pthread_cond_init(cond, NULL) ! 0) { // 处理错误 perror(Failed to initialize condition variable); exit(EXIT_FAILURE); } // 静态初始化 pthread_cond_t cond PTHREAD_COND_INITIALIZER;销毁条件变量代码语言javascriptAI代码解释int pthread_cond_destroy(pthread_cond_t *cond);参数说明cond要销毁的条件变量返回值成功返回0失败返回错误码注意事项只有在没有线程等待该条件变量时才能安全销毁静态初始化的条件变量不需要销毁销毁后不应再使用该条件变量等待条件满足代码语言javascriptAI代码解释int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);参数说明cond要等待的条件变量mutex与条件变量关联的互斥锁返回值成功返回0失败返回错误码关键特性原子操作pthread_cond_wait会原子性地执行以下操作释放互斥锁mutex将线程添加到条件变量的等待队列中使线程进入等待状态唤醒后的操作当线程被唤醒时它会重新获取互斥锁mutex从pthread_cond_wait返回虚假唤醒线程可能会在没有收到明确信号的情况下被唤醒因此必须在循环中检查条件唤醒等待的线程唤醒单个线程代码语言javascriptAI代码解释int pthread_cond_signal(pthread_cond_t *cond);功能唤醒至少一个等待该条件变量的线程具体唤醒哪个线程取决于调度策略使用场景当只有一个线程需要被唤醒时使用效率较高唤醒所有线程代码语言javascriptAI代码解释int pthread_cond_broadcast(pthread_cond_t *cond);功能唤醒所有等待该条件变量的线程使用场景当多个线程需要被唤醒时当不确定哪个线程应该被唤醒时当条件的变化可能影响多个等待线程时示例代码语言javascriptAI代码解释#include iostream #include vector #include string #include pthread.h #include unistd.h #define NUM 5 int cnt 100; pthread_mutex_t glock PTHREAD_MUTEX_INITIALIZER; pthread_cond_t gcond PTHREAD_COND_INITIALIZER; void* threadrun(void* args) { std::string name static_castconst char*(args); while(true) { pthread_mutex_lock(glock); // 获取锁 pthread_cond_wait(gcond, glock); // 等待条件变量会自动释放锁 std::cout name 计算: cnt std::endl; // 被唤醒后执行任务 pthread_mutex_unlock(glock); // 释放锁 } } int main() { std::vectorpthread_t threads; for(int i 0; i NUM; i) { pthread_t tid; char* name new char[64]; snprintf(name, 64, thread-%d, i1); int n pthread_create(tid, nullptr, threadrun, name); if(n ! 0) continue; threads.push_back(tid); sleep(1); } sleep(3); while(true) { std::cout 唤醒一个线程... std::endl; pthread_cond_signal(gcond); sleep(1); } for(auto id : threads) { int n pthread_join(id, nullptr); (void)n;// 返回值不做判断基本都不会失败 } return 0; }1. 初始化阶段静态初始化了一个互斥锁glock和一个条件变量gcond2. 线程创建阶段创建5个线程每个线程间隔1秒启动每个线程执行threadrun函数并传递线程名称作为参数3. 线程执行阶段这是最关键的部分每个线程的执行流程获取互斥锁 (pthread_mutex_lock)调用pthread_cond_wait- 这个函数会原子性地释放互斥锁让其他线程可以获取锁使线程进入等待状态休眠不消耗CPU等待被pthread_cond_signal唤醒被唤醒后自动重新获取互斥锁然后执行任务释放互斥锁然后循环回到步骤14. 主线程唤醒阶段主线程每隔1秒调用pthread_cond_signal每次调用会唤醒一个等待在条件变量上的线程条件变量函数详解1.pthread_cond_wait(gcond, glock)这是条件变量的核心函数它的工作原理很精妙原子性操作释放互斥锁glock让其他线程可以获取锁将当前线程加入到gcond的等待队列中使线程进入等待状态休眠当被唤醒时重新获取互斥锁glock可能会阻塞直到获取到锁从pthread_cond_wait返回继续执行后续代码2.pthread_cond_signal(gcond)唤醒一个等待在条件变量gcond上的线程如果有多个线程在等待具体唤醒哪个取决于调度策略不会立即让被唤醒的线程运行只是将其从等待状态变为可运行状态3. 为什么需要互斥锁配合条件变量必须与互斥锁配合使用因为保护共享数据cnt操作需要互斥保护避免竞态条件确保检查条件和进入等待是原子操作防止虚假唤醒在重新检查条件前保持锁的保护运行结果代码语言javascriptAI代码解释ltxiv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_thread/Cond/TestCond$ ./test 唤醒一个线程... thread-1计算: 100 唤醒一个线程... thread-2计算: 101 唤醒一个线程... thread-3计算: 102 唤醒一个线程... thread-4计算: 103 唤醒一个线程... thread-5计算: 104 唤醒一个线程... thread-1计算: 105 唤醒一个线程... thread-2计算: 106 唤醒一个线程... thread-3计算: 107 唤醒一个线程... thread-4计算: 108 唤醒一个线程... thread-5计算: 109 唤醒一个线程... thread-1计算: 110 唤醒一个线程... thread-2计算: 111 ^C可以看到按序输出这是因为线程按创建顺序依次进入等待状态pthread_cond_signal通常按队列顺序唤醒线程FIFO每次只唤醒一个线程所以执行顺序是确定性的2. 生产者消费者模型2.1 超市购物比喻理解生产者消费者模型让我们用一个超市购物的生动例子来解释生产者消费者模型想象一个超市系统生产者 商品供应商如牛奶厂、面包房消费者 购物顾客交易场所 超市货架和仓库商品 数据工作流程详解正常运营流程缓冲区平衡状态供应商送货→ 商品放入货架 →顾客购买→ 商品从货架取出生产速度≈消费速度→ 系统平稳运行两种阻塞场景详解1. 当货架满时生产者阻塞现实场景送货卡车到达超市仓库仓库管理员对不起仓库满了请在门外稍等卡车司机停车等待不消耗燃油不占用CPU当有顾客买走商品空出位置后卡车先生现在可以卸货了卡车开始卸货2. 当货架空时消费者阻塞现实场景顾客来到超市货架前货架空空如也唉没货了等等吧顾客去喝咖啡休息不浪费时间徘徊不忙等待当供应商补货后顾客们新货到了顾客开始选购商品 三种关键关系1. 生产者与生产者之间的关系竞争关系超市例子多个牛奶供应商都想把产品放到有限的冷藏柜中技术实现需要互斥锁保护共享资源货架空间关系本质互斥- 生产者之间需要竞争有限的缓冲区空间2. 消费者与消费者之间的关系竞争关系超市例子多个顾客都想购买最后一瓶牛奶技术实现需要互斥锁保护共享资源商品关系本质互斥- 消费者之间需要竞争有限的数据/商品3. 生产者与消费者之间的关系同步与协作关系超市例子顾客买走商品后需要通知供应商补货货架满时供应商需要等待空位技术实现使用条件变量进行线程间通信和同步关系本质同步- 生产者和消费者需要协调工作节奏 两种角色1. 生产者职责产生数据/商品并放入缓冲区特点通常有固定的生产节奏关注点缓冲区是否有空位2. 消费者职责从缓冲区取出数据/商品并进行处理特点消费速度可能波动关注点缓冲区是否有数据可消费 一个交易场所缓冲区本质一块特定结构的内存空间通常是队列功能解耦生产者和消费者平衡生产和消费速度差异提供临时存储2.2 为何使用生产者消费者模型1. 解耦Decoupling超市例子牛奶厂不需要知道谁买了牛奶顾客也不需要知道牛奶是哪家厂生产的。他们只关心超市这个中间平台。技术优势生产者和消费者可以独立开发和修改系统更容易维护和扩展降低系统复杂度2. 支持并发Concurrency Support超市例子多个供应商可以同时往不同区域补货多个顾客可以同时在不同区域购物。技术优势生产者线程和消费者线程可以并发执行提高系统吞吐量和资源利用率充分利用多核CPU性能3. 支持忙闲不均Handling Speed Mismatches超市例子牛奶厂每天生产1000瓶奶但顾客有时买得多有时买得少。超市仓库可以缓冲这种不平衡。技术优势缓冲区可以平衡生产和消费的速度差异防止快速生产者淹没慢速消费者防止消费者等待造成资源浪费2.3 基于BlockingQueue的生产者消费者模型BlockingQueue在多线程编程中阻塞队列(Blocking Queue)是一种线程安全的、常用于实现生产者-消费者模型的高级数据结构。与普通队列相比阻塞队列具有以下关键特性阻塞特性当队列为空时消费者线程尝试从队列中获取元素会被阻塞直到队列中有新元素当队列已满时生产者线程尝试向队列中添加元素会被阻塞直到队列中有空位阻塞队列的实现原理阻塞队列通常使用以下组件实现一个普通队列存储元素的数据结构数组或链表一个互斥锁保护对队列的并发访问两个条件变量not_empty当队列为空时消费者线程等待此条件not_full当队列已满时生产者线程等待此条件阻塞队列的工作流程生产者线程的工作流程获取互斥锁检查队列是否已满如果已满等待not_full条件变量否则将元素加入队列释放互斥锁通知消费者线程通过not_empty条件变量消费者线程的工作流程获取互斥锁检查队列是否为空如果为空等待not_empty条件变量否则从队列取出元素释放互斥锁通知生产者线程通过not_full条件变量模拟阻塞队列的生产消费模型注意为便于理解我们先以单生产者-单消费者模型为例进行讲解。初始阶段采用原生接口实现后面再将我们之前封装好的互斥量等进行复用首先实现单生产-单消费模型之后扩展为多生产-多消费模式其实代码逻辑仍然保持不变。封装阻塞队列上文已经提到了阻塞队列的原理那么我们可以通过数据结构队列来实现代码如下代码语言javascriptAI代码解释#include iostream #include queue #include pthread.h const int defaultcap 5; template class T class BlockQueue { public: BlockQueue(int cap defaultcap) :_cap(cap) { pthread_mutex_init(_mutex, nullptr); pthread_cond_init(_full_cond, nullptr); pthread_cond_init(_empty_cond, nullptr); } ~BlockQueue() { pthread_mutex_destroy(_mutex); pthread_cond_destroy(_full_cond); pthread_cond_destroy(_empty_cond); } private: std::queueT _q; size_t _cap; // 队列容量大小 pthread_mutex_t _mutex; pthread_cond_t _full_cond; pthread_cond_t _empty_cond; };生产者生产资源消费者消费资源本质都是对队列的增删查改等操作也就是访问临界资源所以互斥量是需要的由于阻塞特性所以条件变量也是必不可少的。我们知道队列为空时消费者从队列获取数据会被阻塞队列为满时生产者生产数据入队列时也会被阻塞那么我们就需要判断队列的状态是否为空还是满。当然这两个函数我们只需要在内部判断不需要暴露给外部使用可以私有代码语言javascriptAI代码解释private: bool IsFull() { return _q.size() _cap; } bool IsEmpty() { return _q.empty(); }生产者删除数据入队在上文中我们已经知道了流程不过我们在实现时引入了两个成员变量代码语言javascriptAI代码解释private: std::queueT _q; size_t _cap; // 队列容量大小 pthread_mutex_t _mutex; pthread_cond_t _full_cond; pthread_cond_t _empty_cond; int _csleep_num; // 消费者休眠的个数 int _psleep_num; // 生产者休眠的个数 };通过这两个成员变量判断是否有生产者或者消费者在wait阻塞休眠队列满了或者空了有的话我们就唤醒生产者或者消费者代码如下代码语言javascriptAI代码解释// 生产者生产数据入队列 void Enqueue(const T in) { pthread_mutex_lock(_mutex); // 不能使用if判断会虚假唤醒 while(IsFull()) { _psleep_num; std::cout 队列已满, 生产者进入休眠, 生产者休眠个数: _psleep_num std::endl; pthread_cond_wait(_full_cond, _mutex); _psleep_num--; } // 此时队列必定有空间 _q.push(in); // 只有队列为空时消费者才会阻塞休眠此时队列肯定不为空 // 那么就判断是否有消费者休眠有就唤醒 if(_csleep_num 0) { pthread_cond_signal(_empty_cond); std::cout 唤醒消费者... std::endl; } // 直接唤醒其实也可以为什么 //pthread_cond_signal(_empty_cond); pthread_mutex_unlock(_mutex); // 最后直接唤醒也行为什么 //pthread_cond_signal(_empty_cond); }代码实现很简单但是需要注意几个问题如果不使用这两个新增的条件变量直接唤醒也行或者在解锁之后直接唤醒也可以。为什么呢1. 为什么直接唤醒也可以即不管有没有消费者等待都发送信号直接唤醒无条件调用pthread_cond_signal是可以的但可能有性能影响为什么可以pthread_cond_signal是一个轻量级操作如果没有线程在条件变量上等待这个调用实际上什么都不做从功能上讲不会造成任何错误为什么不总是这样做虽然单次调用开销很小但在高并发场景下大量不必要的信号调用会累积成可观的性能开销代码中的条件判断(if(_csleep_num 0))是一种优化避免了不必要的系统调用注意在使用条件变量阻塞等待时会释放锁唤醒之后会重新申请锁但是此时也有可能锁被别人申请了那么这个时候在申请锁时被阻塞等待。2. 为什么在解锁后发送信号也可以在解锁后发送信号是完全可行且有时是更好的做法为什么可以POSIX允许在持有或不持有互斥锁的情况下调用pthread_cond_signal条件变量的信号操作本身是线程安全的为什么有时更好减少锁持有时间先解锁再发信号减少了互斥锁的持有时间避免立即竞争如果先发信号再解锁被唤醒的线程会立即尝试获取锁导致锁竞争提高性能被唤醒的线程可以立即获取到CPU时间片而不是等待当前线程释放锁潜在风险如果在解锁后发送信号需要确保状态的一致性不会被破坏在我们的例子中由于队列操作已经完成解锁后发送信号是安全的3. 为什么使用if会造成虚假唤醒问题问题pthread_cond_wait是函数调用那么函数就有可能调用失败万一失败那此时队列为满并没有进行等待阻塞而是直接push把数据入队列那不就出问题了吗或者如果是多生产单消费消费者消费完一个数据然后广播唤醒了所有生产者那所有生产者都会push数据不也会出问题吗首先什么是虚假唤醒虚假唤醒是指线程在没有收到明确的信号或广播的情况下从pthread_cond_wait中返回的现象。这不是 bug而是 POSIX 标准允许的行为原因包括性能优化某些实现可能为了性能而允许虚假唤醒信号中断线程可能被系统信号中断硬件因素多处理器环境下的内存一致性模型但是如果使用while循环判断就不会出现这些问题而是会重新检查IsFull()发现队列又满了会再次进入等待。消费者消费数据出队列逻辑和生产者生产数据入队列一样代码如下代码语言javascriptAI代码解释// 消费者消费数据出队列 T Pop() { pthread_mutex_lock(_mutex); while(IsEmpty()) { _csleep_num; std::cout 队列为空, 消费者进入休眠, 消费者休眠个数: _csleep_num std::endl; pthread_cond_wait(_empty_cond, _mutex); _csleep_num--; } // 此时队列必定有空间 T data _q.front(); _q.pop(); // 只有队列为空时消费者才会阻塞休眠此时队列肯定不为空 // 那么就判断是否有消费者休眠有就唤醒 if(_psleep_num 0) { pthread_cond_signal(_full_cond); std::cout 唤醒生产者... std::endl; } pthread_mutex_unlock(_mutex); return data; }主程序阻塞队列已经封装好了接下来就需要在主程序中编写测试单生产单消费模型代码语言javascriptAI代码解释#include BlockQueue.hpp #include unistd.h void* consumer(void* args) { BlockQueueint* bq static_castBlockQueueint*(args); while(true) { int data bq-Pop(); std::cout 消费了一个数据: data std::endl; } } void* producer(void* args) { int data 1; BlockQueueint* bq static_castBlockQueueint*(args); while(true) { sleep(1); std::cout 生产了一个数据: data std::endl; bq-Enqueue(data); data; } } int main() { BlockQueueint* bq new BlockQueueint; pthread_t c, p; pthread_create(c, nullptr, consumer, bq); pthread_create(p, nullptr, producer, bq); pthread_join(c, nullptr); pthread_join(p, nullptr); return 0; }运行结果代码语言javascriptAI代码解释ltxiv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_thread/Cond/PC$ ./pc 队列为空, 消费者进入休眠, 消费者休眠个数: 1 生产了一个数据: 1 唤醒消费者... 消费了一个数据: 1 队列为空, 消费者进入休眠, 消费者休眠个数: 1 生产了一个数据: 2 唤醒消费者... 消费了一个数据: 2 队列为空, 消费者进入休眠, 消费者休眠个数: 1 生产了一个数据: 3 唤醒消费者... 消费了一个数据: 3 队列为空, 消费者进入休眠, 消费者休眠个数: 1 生产了一个数据: 4 唤醒消费者... 消费了一个数据: 4 队列为空, 消费者进入休眠, 消费者休眠个数: 1 生产了一个数据: 5 唤醒消费者... 消费了一个数据: 5 队列为空, 消费者进入休眠, 消费者休眠个数: 1 生产了一个数据: 6 唤醒消费者... 消费了一个数据: 6 队列为空, 消费者进入休眠, 消费者休眠个数: 1 生产了一个数据: 7 唤醒消费者... 消费了一个数据: 7 队列为空, 消费者进入休眠, 消费者休眠个数: 1 生产了一个数据: 8 唤醒消费者... 消费了一个数据: 8 队列为空, 消费者进入休眠, 消费者休眠个数: 1 ^C我们也可以来试一下队列为满的情况其他代码不变先让消费者sleep上10秒钟让生产者把队列push满代码语言javascriptAI代码解释void* consumer(void* args) { BlockQueueint* bq static_castBlockQueueint*(args); while(true) { sleep(10); int data bq-Pop(); std::cout 消费了一个数据: data std::endl; } } void* producer(void* args) { int data 1; BlockQueueint* bq static_castBlockQueueint*(args); while(true) { //sleep(1); std::cout 生产了一个数据: data std::endl; bq-Enqueue(data); data; } }运行结果代码语言javascriptAI代码解释ltxiv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_thread/Cond/PC$ ./pc 生产了一个数据: 1 生产了一个数据: 2 生产了一个数据: 3 生产了一个数据: 4 生产了一个数据: 5 生产了一个数据: 6 队列已满, 生产者进入休眠, 生产者休眠个数: 1 唤醒生产者... 消费了一个数据: 1 生产了一个数据: 7 队列已满, 生产者进入休眠, 生产者休眠个数: 1 唤醒生产者... 消费了一个数据: 2 生产了一个数据: 8 队列已满, 生产者进入休眠, 生产者休眠个数: 1 ^C对于多生产多消费模型我们的阻塞队列代码并不需要改变其实原理都是一样的因为不管是谁访问队列都需要互斥访问。注意这里使用模板是为了说明队列中不仅可以存放内置类型如int对象同样可以作为任务参与生产消费流程。3. 为什么 pthread_cond_wait 需要互斥量?基本原理条件等待是多线程编程中实现线程同步的重要手段。它的核心逻辑是当一个线程发现某个条件不满足时主动进入等待状态直到其他线程修改了共享变量使得条件满足并通过信号唤醒等待线程。这种机制必须满足以下两个基本要素共享变量的修改必须有至少一个线程能够修改影响条件的共享变量互斥保护所有对共享变量的访问和修改都必须通过互斥锁进行保护错误实现示例分析考虑以下看似合理的错误实现代码语言javascriptAI代码解释// 错误的设计 pthread_mutex_lock(mutex); while (condition_is_false) { pthread_mutex_unlock(mutex); //解锁之后等待之前条件可能已经满⾜信号已经发出但是该信号可能被错过 pthread_cond_wait(cond, mutex); pthread_mutex_lock(mutex); } pthread_mutex_unlock(mutex);这个设计存在严重的竞态条件问题在解锁后到调用pthread_cond_wait之前存在时间窗口其他线程可能在此期间获取锁、修改条件并发送信号这会导致信号丢失等待线程可能永远阻塞假设有两个线程消费者线程C和生产者线程P时间消费者线程C生产者线程P问题描述t1pthread_mutex_lock(mutex)等待锁C获取锁t2while (condition_is_false) → true等待锁条件不满足t3pthread_mutex_unlock(mutex)等待锁C释放锁t4时间窗口开始pthread_mutex_lock(mutex)P获取锁t5准备调用 pthread_cond_wait修改条件为trueP改变条件t6pthread_cond_signal(cond)P发送信号t7pthread_mutex_unlock(mutex)P释放锁t8调用 pthread_cond_wait(cond, mutex)信号已错过t9永久阻塞...线程死锁正确的原子性操作正确的实现要求解锁和等待必须是原子操作这正是pthread_cond_wait的设计目的函数原型int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);内部机制检查条件量是否为0将互斥量置为1解锁进入等待状态被唤醒后将条件量置为1恢复互斥量原状态4. 封装条件变量和封装互斥量一样非常简单代码如下代码语言javascriptAI代码解释#pragma once #include cstring #include Mutex.hpp using namespace MutexModule; namespace CondModule { class Cond { public: Cond() { int n pthread_cond_init(_cond, nullptr); if (n ! 0) { std::cerr cond init failed: strerror(n) std::endl; } } void Wait(Mutex mutex) { int n pthread_cond_wait(_cond, mutex.Get()); if (n ! 0) { std::cerr cond wait failed: strerror(n) std::endl; } } void Signal() { int n pthread_cond_signal(_cond); if (n ! 0) { std::cerr cond signal failed: strerror(n) std::endl; } } void Broadcast() { int n pthread_cond_broadcast(_cond); if (n ! 0) { std::cerr cond broadcast failed: strerror(n) std::endl; } } ~Cond() { int n pthread_cond_destroy(_cond); if (n ! 0) { std::cerr cond destroy failed: strerror(n) std::endl; } } private: pthread_cond_t _cond; }; }为了提高条件变量的通用性建议在封装Cond类时避免直接引用内部的互斥量。这样可以在后续组合使用时避免因代码耦合导致的初始化困难因为Mutex和Cond通常需要同时创建。我们给互斥量新增一个接口用于条件变量中需要wait获得锁的情况代码语言javascriptAI代码解释pthread_mutex_t* Get() { return _mutex; }下面我们也可以将阻塞队列修改一下将封装的互斥量和条件变量复用起来代码语言javascriptAI代码解释#include iostream #include queue #include pthread.h #include Cond.hpp #include Mutex.hpp using namespace MutexModule; using namespace CondModule; const int defaultcap 5; template class T class BlockQueue { private: bool IsFull() { return _q.size() _cap; } bool IsEmpty() { return _q.empty(); } public: BlockQueue(int cap defaultcap) :_cap(cap), _csleep_num(0), _psleep_num(0) {} // 生产者生产数据入队列 void Enqueue(const T in) { LockGuard lockguard(_mutex); // 不能使用if判断会虚假唤醒 while(IsFull()) { _psleep_num; std::cout 队列已满, 生产者进入休眠, 生产者休眠个数: _psleep_num std::endl; _full_cond.Wait(_mutex); _psleep_num--; } // 此时队列必定有空间 _q.push(in); // 只有队列为空时消费者才会阻塞休眠此时队列肯定不为空 // 那么就判断是否有消费者休眠有就唤醒 if(_csleep_num 0) { _empty_cond.Signal(); std::cout 唤醒消费者... std::endl; } } // 消费者消费数据出队列 T Pop() { LockGuard lockguard(_mutex); while(IsEmpty()) { _csleep_num; std::cout 队列为空, 消费者进入休眠, 消费者休眠个数: _csleep_num std::endl; _empty_cond.Wait(_mutex); _csleep_num--; } // 此时队列必定有空间 T data _q.front(); _q.pop(); // 只有队列为空时消费者才会阻塞休眠此时队列肯定不为空 // 那么就判断是否有消费者休眠有就唤醒 if(_psleep_num 0) { _full_cond.Signal(); std::cout 唤醒生产者... std::endl; } return data; } ~BlockQueue() {} private: std::queueT _q; size_t _cap; // 队列容量大小 Mutex _mutex; Cond _full_cond; Cond _empty_cond; int _csleep_num; // 消费者休眠的个数 int _psleep_num; // 生产者休眠的个数 };