线程同步与互斥(下)
线程同步与互斥中https://blog.csdn.net/Small_entreprene/article/details/147003513?fromshareblogdetailsharetypeblogdetailsharerId147003513sharereferPCsharesourceSmall_entreprenesharefromfrom_link我们学习了互斥紧接着认识到什么是同步同步是为了什么对于上一篇还保留了一个问题还没算真正被解决同步为什么要用到锁呢除了上一篇的部分解释wait 操作到底是为什么需要传锁在此之前我们需要来封装一下条件变量条件变量的封装通过封装 pthread 当中的 cond 条件变量我们可以显著简化多线程编程中的条件等待和通知逻辑。封装不仅提高了代码的可读性和可维护性还减少了手动管理锁带来的潜在问题。在实际开发中合理使用条件变量封装可以显著提升程序的性能和可靠性。#pragma once #include iostream #include pthread.h #include Mutex.hpp using namespace MutexModule; namespace CondModule { class Cond { public: Cond() { pthread_cond_init(_cond, nullptr); } void Wait(Mutex mutex) { int n pthread_cond_wait(_cond, mutex.Get()); (void)n; } void Signal() { // 唤醒在条件变量下等待的一个线程 int n pthread_cond_signal(_cond); (void)n; } void Broadcast() { // 唤醒所有在条件变量下等待的线程 int n pthread_cond_broadcast(_cond); (void)n; } ~Cond() { pthread_cond_destroy(_cond); } private: pthread_cond_t _cond; }; };代码中的头文件是之前文章中的封装所以我们就可以使用我们自己封装的 Mutex 和 Cond对上一篇的生产者消费者模型做一下头文件的使用还有内容的稍加修改BlockQueue.hpp:// 阻塞队列的实现 #pragma once #include iostream #include string #include queue #include pthread.h #include unistd.h #include Mutex.hpp #include Cond.hpp const int defaultcap 10; // for test using namespace MutexModule; using namespace CondModule; template class T class BlockQueue { private: bool IsFull() { return _q.size() _cap; } bool IsEmpty() { return !_q.size(); } public: BlockQueue(int cap defaultcap) : _cap(cap), _csleep_num(0), _psleep_num(0) { // 锁已经被我们封装了 // 条件变量已经被我们封装了 } void Equeue(const T in) { { // lockguard就是将下面的整个临界区保护起来了使用封装来实现了锁的自动构造加锁和析构解锁因为lockguard是临时变量的生命周期是只在自己的作用域当中 // 下面判断和push都是访问临界资源因为可能一个线程在入队列的时候其他的线程在出队列我们应该加锁 LockGuard lockguard(_mutex); // 生产者调用 // if (IsFull())//BUG!!!!! while (IsFull()) // 增加代码的健壮性 { // 商品满了需要等待不然都没有位置放了 // 重点1: 在临界区内休眠可别将锁一起带去休眠了 // 重点2: 当线程被唤醒的时候也是重wait出来继续向后运行的这就默认了就在临界区当中唤醒但是之前锁不是被释放了吗所以该线程要从pthread_cond_wait中成功返回就需要当前线程重新申请锁 // 重点3: 如果阻塞的线程被唤醒但是申请锁失败了就会在锁上阻塞等待 _psleep_num; _full_cond.Wait(_mutex); _psleep_num--; } // 100%是队列有空间了 _q.push(in); // 到这里就一定有数据这就可以唤醒消费者来消费了 // 临时方案后续优化 if (_csleep_num 0) { // 别睡了快来消费 _empty_cond.Signal(); std::cout 唤醒消费者... std::endl; } } } T Pop() { T data; { // 消费者调用 LockGuard lockguard(_mutex); while (IsEmpty()) { _csleep_num; _empty_cond.Wait(_mutex); _csleep_num--; } data _q.front(); _q.pop(); // 消费者到这说明已经消费了就一定有空间所以可以唤醒生产者进行生产了 if (_psleep_num 0) { // 别睡了快来生产 _full_cond.Signal(); std::cout 唤醒生产者... std::endl; } return data; } } ~BlockQueue() { // 锁已经被我们封装了 // 条件变量已经被我们封装了 } private: std::queueT _q; // 临界资源 int _cap; // 容量大小 Mutex _mutex; // 锁 Cond _full_cond; // 生产者生产满了就把自己放在条件变量下 Cond _empty_cond; // 消费者消费完了就把自己放在条件变量下 int _csleep_num; // 消费者休眠的个数 int _psleep_num; // 生产者休眠的个数 };我们来测试一下代码#include BlockQueue.hpp #include Task.hpp #include iostream #include pthread.h #include unistd.h void *consumer(void *args) { BlockQueuetask_t *bq static_castBlockQueuetask_t *(args); while (true) { sleep(3); // 1. 消费任务 task_t t bq-Pop(); // 2. 处理任务 -- 处理任务的时候这个任务已经被拿到线程的上下文中了,不属于队列了 t(); } } void *productor(void *args) { BlockQueuetask_t *bq static_castBlockQueuetask_t *(args); while (true) { // 1. 获得任务 std::cout 生产了一个任务: std::endl; // 2. 生产任务 bq-Equeue(Download); } } int main() { // 扩展认识: 阻塞队列: 可以放任务吗 // 申请阻塞队列 BlockQueuetask_t *bq new BlockQueuetask_t(); // 构建生产和消费者 pthread_t c[2], p[3]; pthread_create(c, nullptr, consumer, bq); // pthread_create(c 1, nullptr, consumer, bq); pthread_create(p, nullptr, productor, bq); // pthread_create(p 1, nullptr, productor, bq); // pthread_create(p 2, nullptr, productor, bq); pthread_join(c[0], nullptr); // pthread_join(c[1], nullptr); pthread_join(p[0], nullptr); // pthread_join(p[1], nullptr); // pthread_join(p[2], nullptr); return 0; }后续我们就可以拿着这些封装来实现相关代码。POSIX 信号量回顾一下相关概念我们在进程间信号的时候谈到了信号量这个 System V 版本的信号量对于我们今天要说的信号量其实也是相同的理论。system V版本【信号量回顾放映厅形象解释】https://blog.csdn.net/Small_entreprene/article/details/146120541?fromshareblogdetailsharetypeblogdetailsharerId146120541sharereferPCsharesourceSmall_entreprenesharefromfrom_linkPOSIX 是一种新的标准和 System V 是类似的只不过 POSIX 是更加常用的反而是 System V 版本是快被淘汰了。信号量也被称为信号灯和信号根本没有联系信号是一种进程间异步通知的方式而信号量的本质是一个计数器用来表示临界资源当中资源的数量有多少。对于放映厅的例子我们只要将票买成功了那么对应的位置就是属于我的了哪怕今天我不去看这场电影了放映厅这个位置也是必须要为我留的。所以信号量/信号灯本质就是一个计数器是对待定资源的预定机制。放映厅本身就是临界资源我们看电影前买票在技术人员看来就是保证放映厅内部资源的安全性但是有一点点不一样信号量是描述临界资源当中资源数量有多少假设放映厅中有50个座位信号量的计数器就是50申请一个50 --当信号量被减到0时就表明当前资源已经是被占用满的了不能够再申请了。这样我们就能够保证进到放映厅的人数不会超过座位数最多50然后再经过合理的调度让不同的线程坐到不同的位置上这样线程就可以访问放映厅的同时访问放映厅内不同的座位了。我们说的这个放映厅的例子是将整个放映厅局部进行访问也就是说我们允许放映厅当中的座位被多个不同的人同时访问不同的位置虽然整个放映厅是一个大的临界资源但只要每一个人访问不同的小的资源那么大家就不冲突。那么如果有一个 VIP 放映厅里面只有一个座位所以对应的信号量计数器的值就是1你只要申请成功的话这个 VIP 放映厅就是你的申请失败就要等我们将这种计数器值为1的信号量我们称之为二元信号量二元信号量的本质不就是互斥嘛幸运的是我们目前已经学习了锁了所以多线程使用资源有两种场景将目标资源整体使用使用锁计数/二元信号量上面我们按照条件变量所写的阻塞队列就是将阻塞队列这一资源整体使用的所以我们要加锁将目标资源按照不同的“块”分批使用 如果可以将一个整体资源划分成一块一块的局部资源就可以往这些局部资源中放入线程实现并发访问我们申请信号量本质是让所有线程都可见这个信号量对象。信号量的核心是一个资源计数器但这个计数器不能是普通的整型变量count—— 因为count/count--不是原子操作多线程下会出现数据错乱。合理的方案是给普通整型count加一把互斥锁封装起来就可以实现信号量的基础功能。但原生信号量的实现远比这更复杂、高效它不仅要保证计数器的原子性还要实现线程阻塞 / 唤醒机制申请资源时执行sem--若计数器≤0线程会阻塞等待释放资源时执行sem并唤醒等待的线程。所以信号量本质也是临界资源对应的P操作就是对应信号量的 -- 操作一定是原子性的V操作就是对应信号量的 操作一定是原子性的基于环形队列的生产消费模型环形队列是为了突出信号量的特征环形队列采用数组或链表模拟用模运算来模拟环形状态环形队列为空代表head指针 tail指针环形队列为满也是代表head指针 tail指针我们应该清楚写这种数据结构的时候少不了对该数据结构进行判空或判满很明显空和满都是head tail; 的所以我们需要有空和满的解决方案方案 1使用计数器int count默认为 0如果环形队列入数据那么相应的count; 出数据count--; 所以count为 0 表示环形队列为空count为环形队列的容量要求时该环形队列为满方案 2预留一个空的位置作满的状态下面是解释head tail默认表示的就是环形队列是空的将来生产入队列的时候在不考虑消费出队列的过程的时候tail的位置是处于内容为空的也就是没有生产任务但是是准备生产任务状态是否生产任务需要判断tail的当前位置的下一个位置是否是head不相等的话就说明tail可以生产因为没满。也就是tail的位置就是将要生产放置的位置if (head tail) { // 空状态 } else if ((tail 1) % n head) { // 满状 } else { // 可生产状态 }我们环形队列是可以使用链表实现但是没有必要我们就按照数组模拟实现固定大小的环形队列通过取模来实现位置的正确变换对于我们上面的判空/判满的两种方案其实对于今天的生产者消费者模型来说是没有用到的因为我们还有另外一种方案信号量我们形象一点我们将环形队列看成一个大圆桌每个位置只能放一个苹果生产任务为了实现生产者消费者模型我们有如下几个约定【约定 1】: 队列为空生产者先运行【约定 2】: 队列为满消费者先运行【约定 3】: 生产者不能把消费者套一个圈以上否者会将旧任务覆盖成新任务【约定 4】: 消费者不能超过生产者否则根本就没有苹果给你消费所以遵守上面的约定生产者和消费者不访问同一个位置两者就可以同时运行。【这个不就效率上来了嘛】那么什么时候两者会在同一位置呢为空或者为满的时候也就是说如果不为空或者不为满的时候两者的操作就可以同时运行为空只能【互斥】生产者先【同步】运行为满只能【互斥】消费者先【同步】运行这两个很好体现了互斥和同步之间的关系结论遵守4个约定下环形队列不为空 不为满生产消费可以同时运行环形队列【为空 || 为满】的时候生产和消费需要同步和互斥。我们是使用信号量来保证上面的4个约定信号量是表示临界资源数目的所以对于生产者关心的资源环形队列中空的位置初始sem_blank N;(N是环形队列的大小)对于消费者关心的资源环形队列中的有效数据有数据初始sem_data 0;我们下面来模拟一下生产消费的追逐过程伪代码形式//《《《《生产者》》》》 int p_step 0; // 生产者起始位置下标 P(sem_blank); // P操作: sem_blank-- // 【这里应该放入数据】 p_step (p_step 1) % N; // 走到下一个空位置 V(sem_data); // V操作: sem_data //《《《《消费者》》》》 int c_step 0; // 消费者起始位置下标 P(sem_data); // P操作: sem_data-- // 【这里应该取出数据】 c_step (c_step 1) % N; // 走到下一个有效位置 V(sem_blank); // V操作: sem_blank信号量的 P 操作是原子的申请成功继续运行申请失败申请的线程会被阻塞刚开始sem_data为 0sem_blank大于 0消费者就会在对应的P(sem_data)位置进行阻塞生产者是可以进行 P 操作这就是让队列为空的时候生产者先运行生产满了也是如此而且 PV 操作是原子的所以如上的 4 个约定我们就可以遵守了。【这个伪代码中间过程安全性就无法保证了】POSIX 版的信号量的接口介绍初始化信号量#include semaphore.h int sem_init(sem_t *sem, int pshared, unsigned int value);参数sem指向信号量的指针。pshared0表示线程间共享非零表示进程间共享。value信号量的初始值。销毁信号量int sem_destroy(sem_t *sem);参数sem指向要销毁的信号量的指针。等待信号量int sem_wait(sem_t *sem); // P()操作功能等待信号量会将信号量的值减1。参数sem指向信号量的指针。发布信号量int sem_post(sem_t *sem); // V()操作功能发布信号量表示资源使用完毕可以归还资源了。将信号量值加1。参数sem指向信号量的指针。我们可以发现其实信号量的接口跟条件变量是很相似的结构的。sem 信号量的封装Sem.hpp#include iostream #include semaphore.h #include pthread.h // 定义一个命名空间SemModule用于封装信号量相关的操作 namespace SemModule { // 定义一个常量defaultvalue表示信号量的默认初始值 const int defaultvalue 1; // 定义一个类Sem用于封装POSIX信号量的操作 class Sem { public: // 构造函数初始化信号量 // 参数 // sem_value信号量的初始值默认为defaultvalue Sem(unsigned int sem_value defaultvalue) { sem_init(_sem, 0, sem_value); } // P操作等待信号量会将信号量的值减1 // 这是一个原子操作 void P() { int n sem_wait(_sem); // 原子的 (void)n; // 忽略返回值防止编译器警告 } // V操作发布信号量表示资源使用完毕可以归还资源了。将信号量值加1 // 这是一个原子操作 void V() { int n sem_post(_sem); // 原子的 } // 析构函数销毁信号量 ~Sem() { sem_destroy(_sem); } private: // 信号量的内部表示 sem_t _sem; }; }这段代码定义了一个名为Sem的类用于封装POSIX信号量的操作。类中包含了信号量的初始化、P操作等待信号量、V操作发布信号量以及析构函数销毁信号量。这些操作都是原子的确保了线程安全。我们后续可以利用。基于环形队列的生产消费模型实现基于信号量环形队列RingQueue.hpp#pragma once #include iostream #include vector #include Sem.hpp // 定义全局常量gcap用于调试时设置队列容量 static const int gcap 5; // for debug // 使用SemModule命名空间简化代码书写 using namespace SemModule; // 定义RingQueue模板类用于实现环形队列 template typename T class RingQueue { public: // 构造函数初始化环形队列 // 参数 // cap队列容量默认为gcap RingQueue(int cap gcap) : _cap(cap), _rq(cap), _blank_sem(cap), // 初始化空位置信号量初始值为cap _p_step(0), // 初始化生产者步骤下标 _data_sem(0), // 初始化数据信号量初始值为0 _c_step(0) // 初始化消费者步骤下标 { } // 入队操作生产者使用 void Equeue(const T in) { // 1. 申请空位置信号量等待有空位置 _blank_sem.P(); // 2. 生产数据放入队列 _rq[_p_step] in; // 3. 更新生产者步骤下标 _p_step; // 4. 维持环形特性取模操作 _p_step % _cap; // 5. 发布数据信号量通知消费者有新数据 _data_sem.V(); } // 出队操作消费者使用 void Pop(T *out) { // 1. 申请数据信号量等待有数据 _data_sem.P(); // 2. 消费数据从队列中取出 *out _rq[_c_step]; // 3. 更新消费者步骤下标 _c_step; // 4. 维持环形特性取模操作 _c_step % _cap; // 5. 发布空位置信号量通知生产者有空位置 _blank_sem.V(); } private: std::vectorT _rq; // 环形队列的存储 int _cap; // 队列容量 // 生产者相关 Sem _blank_sem; // 空位置信号量 int _p_step; // 生产者步骤下标 // 消费者相关 Sem _data_sem; // 数据信号量 int _c_step; // 消费者步骤下标 };这段代码定义了一个名为RingQueue的模板类用于实现一个线程安全的环形队列。队列使用信号量和互斥锁来同步生产者和消费者之间的操作确保了线程安全。生产者使用Equeue方法入队消费者使用Pop方法出队。信号量用于控制空位置和数据的同步互斥锁用于保护临界区防止数据竞争。在单生产、单消费场景下这个环形队列之所以安全核心是信号量的 “同步 互斥” 双重作用单线程操作索引的原子性错觉再加上环形结构的逻辑隔离哪怕极端并发也不会出现数据竞争或逻辑错乱。不使用信号量、单生产单消费消费者可能比生产者快直接读取空队列里的无效数据生产者可能比消费者快写满队列后覆盖未消费数据逻辑完全错乱。首先信号量直接堵死了 “同时操作共享资源” 的可能_blank_sem控制生产者的 “写入权限”_data_sem控制消费者的 “读取权限”。单生产者每次入队前必须先过_blank_sem.P()—— 只有队列有空位信号量计数 0才能进入而单线程的写入 索引更新_p_step是连续操作不会被其他生产者打断【这里注意是由单生产带来的安全效果】同理单消费者必须过_data_sem.P()—— 只有队列有数据信号量计数 0才能读取读取 _c_step更新也不会被其他消费者打断。再看极端情况比如队列刚满_blank_sem0、_data_sem5生产者想继续入队会被_blank_sem.P()阻塞此时消费者执行出队取走数据后调用_blank_sem.V()信号量计数变 1生产者才被唤醒此时队列已有空位写入不会覆盖未读数据。反过来队列空时_data_sem0消费者会阻塞在_data_sem.P()直到生产者写入后调用_data_sem.V()才被唤醒不会出现 “读空”。还要注意索引的安全性_p_step只有生产者改_c_step只有消费者改单线程对整型的 “自增 取模”_p_step; _p_step%_cap本身是 “逻辑原子”—— 哪怕底层指令可能拆分但单线程下不会被其他线程插入操作最终结果一定正确。而信号量的P/V是操作系统保证的原子操作不会出现 “两个线程同时抢到信号量” 的情况进一步锁死了临界区的唯一访问权。总结来说单生产 单消费下信号量解决了 “生产 / 消费的顺序同步”有空才写、有数据才读和 “临界区的互斥访问”同一时间只有一个线程操作队列再加上索引由单一线程修改、无交叉竞争哪怕极端阻塞 / 唤醒交替也不会出现数据覆盖、索引错乱等线程安全问题。我们这是基于单生产 - 单消费的模型下面我们来测试一下Main.cc#include iostream #include pthread.h #include unistd.h #include RingQueue.hpp struct threaddata { RingQueueint *rq; std::string name; }; void *consumer(void *args) { threaddata *td static_castthreaddata *(args); while (true) { sleep(1); // 1. 消费任务 int t 0; td-rq-Pop(t); // 2. 处理任务 -- 处理任务的时候这个任务已经被拿到线程的上下文中了,不属于队列了 std::cout td-name 消费者拿到了一个数据: t std::endl; // t(); } } int data 1; void *productor(void *args) { threaddata *td static_castthreaddata *(args); while (true) { sleep(1); // sleep(2); // 1. 获得任务 // std::cout 生产了一个任务: x y ? std::endl; std::cout td-name 生产了一个任务: data std::endl; // 2. 生产任务 td-rq-Equeue(data); data; } } int main() { // 扩展认识: 阻塞队列: 可以放任务吗 // 申请阻塞队列 RingQueueint *rq new RingQueueint(); // 构建生产和消费者 // 单单: cc, pp - 互斥关系不需要维护互斥与同步 pthread_t c[2], p[3]; threaddata *td new threaddata(); td-name cthread-1; td-rq rq; pthread_create(c, nullptr, consumer, td); threaddata *td3 new threaddata(); td3-name pthread-2; td3-rq rq; pthread_create(p, nullptr, productor, td3); pthread_join(c[0], nullptr); pthread_join(p[0], nullptr); return 0; }单单的测试结果根据 321 原则21 我们已经是实现了的2生产消费1一个环形队列对于单生产单消费生产者之间和消费者之间的互斥是不需要我们维护的我们使用的环形队列中使用信号量是维护了生产者和消费者之间的同步执行有先后与互斥访问同一资源只能一方访问因为信号量为 0 就可以阻塞住但是如果是多生产多消费呢同样的我们根据 321 原则我们需要多对生产者之间和消费者之间的互斥关系和上一篇的不一样我们这里和上一篇的不同是没有加锁所以我们的RingQueue.hpp需要改这里我们有一个问题先申请信号量再加锁还是先加锁再申请信号量先申请锁就是先选择线程来允许访问临界区资源再申请信号量申请信号量本质就是对资源的预定机制先申请信号量再加锁的顺序首先确保有足够的资源可供使用通过信号量控制然后再确保对这些资源的独占访问通过锁实现。就举放映厅的例子对于先申请锁再申请信号量的情形我们是需要现在放映厅门口排队的因为进入放映厅是需要一个个进入同步机制等轮到你了申请锁成功再打开微信进行买票买成功了申请信号量成功大家就可以进来买失败了后面大家都要等。对于先申请信号量再申请锁的情形我们所有的人要去放映厅看电影我们所有人先将电影票买了申请信号量对有限的电影票进行瓜分没有抢到电影票的人就不可能进来就没有资格在门口排队买了票的人在依次在放映厅门口排队申请锁。上面很明显是后者的效率高因为信号量的申请是原子的而且不会超申请资源有限对于前者先申请锁那么申请锁竞争锁成功的人还需要申请信号量更关键的是申请锁失败的线程还需要将锁给释放了别的线程才有机会获取锁后者就不一样了申请信号量申请锁成功了的线程访问临界区资源其他线程也没闲着都在申请信号量预定自己的资源。【重点知识】总结来说“先申请信号量再加锁” 的顺序首先通过信号量确保有足够的资源可供使用然后再通过锁确保对这些资源的独占访问。这种顺序可以有效地避免资源冲突和数据不一致的问题同时提高资源的利用率。所以我们使用后者#pragma once #include iostream #include vector #include Sem.hpp #include Mutex.hpp // 定义全局常量gcap用于调试时设置队列容量 static const int gcap 5; // for debug // 使用SemModule和MutexModule命名空间简化代码书写 using namespace SemModule; using namespace MutexModule; // 定义RingQueue模板类用于实现环形队列 template typename T class RingQueue { public: // 构造函数初始化环形队列 // 参数 // cap队列容量默认为gcap RingQueue(int cap gcap) : _cap(cap), _rq(cap), _blank_sem(cap), // 初始化空位置信号量初始值为cap _p_step(0), // 初始化生产者步骤下标 _data_sem(0), // 初始化数据信号量初始值为0 _c_step(0) // 初始化消费者步骤下标 { } // 入队操作生产者使用 void Equeue(const T in) { // 1. 申请空位置信号量等待有空位置 _blank_sem.P(); { // 使用锁保护临界区确保线程安全 LockGuard lockguard(_pmutex); // 2. 生产数据放入队列 _rq[_p_step] in; // 3. 更新生产者步骤下标 _p_step; // 4. 维持环形特性取模操作 _p_step % _cap; } // 5. 发布数据信号量通知消费者有新数据 _data_sem.V(); } // 出队操作消费者使用 void Pop(T *out) { // 1. 申请数据信号量等待有数据 _data_sem.P(); { // 使用锁保护临界区确保线程安全 LockGuard lockguard(_cmutex); // 2. 消费数据从队列中取出 *out _rq[_c_step]; // 3. 更新消费者步骤下标 _c_step; // 4. 维持环形特性取模操作 _c_step % _cap; } // 5. 发布空位置信号量通知生产者有空位置 _blank_sem.V(); } private: std::vectorT _rq; // 环形队列的存储 int _cap; // 队列容量 // 生产者相关 Sem _blank_sem; // 空位置信号量 int _p_step; // 生产者步骤下标 // 消费者相关 Sem _data_sem; // 数据信号量 int _c_step; // 消费者步骤下标 // 维护多生产者和多消费者同步需要两把锁 Mutex _cmutex; // 消费者锁 Mutex _pmutex; // 生产者锁 };接下来我们来测试一下多生产多消费的情形Main.cc#include iostream #include pthread.h #include unistd.h #include RingQueue.hpp struct threaddata { RingQueueint *rq; std::string name; }; void *consumer(void *args) { threaddata *td static_castthreaddata *(args); while (true) { sleep(3); // 1. 消费任务 int t 0; td-rq-Pop(t); // 2. 处理任务 -- 处理任务的时候这个任务已经被拿到线程的上下文中了,不属于队列了 std::cout td-name 消费者拿到了一个数据: t std::endl; // t(); } } int data 1; void *productor(void *args) { threaddata *td static_castthreaddata *(args); while (true) { sleep(1); // sleep(2); // 1. 获得任务 // std::cout 生产了一个任务: x y ? std::endl; std::cout td-name 生产了一个任务: data std::endl; // 2. 生产任务 td-rq-Equeue(data); data; } } int main() { // 扩展认识: 阻塞队列: 可以放任务吗 // 申请阻塞队列 RingQueueint *rq new RingQueueint(); // 构建生产和消费者 // 如果我们改成多生产多消费呢 // 单单: cc, pp - 互斥关系不需要维护互斥与同步 // 多多cc, pp - 之间的互斥关系 pthread_t c[2], p[3]; threaddata *td new threaddata(); td-name cthread-1; td-rq rq; pthread_create(c, nullptr, consumer, td); threaddata *td2 new threaddata(); td2-name cthread-2; td2-rq rq; pthread_create(c 1, nullptr, consumer, td2); threaddata *td3 new threaddata(); td3-name pthread-3; td3-rq rq; pthread_create(p, nullptr, productor, td3); threaddata *td4 new threaddata(); td4-name pthread-4; td4-rq rq; pthread_create(p 1, nullptr, productor, td4); threaddata *td5 new threaddata(); td5-name pthread-5; td5-rq rq; pthread_create(p 2, nullptr, productor, td5); pthread_join(c[0], nullptr); pthread_join(c[1], nullptr); pthread_join(p[0], nullptr); pthread_join(p[1], nullptr); pthread_join(p[2], nullptr); return 0; }测试结果多生产多消费打印出来的现象的参考价值不大因为生产消费的时候因为打印是处理的不属于生产或消费打印出来错乱是很正常的等后面我们将日志带进来就好了。不过至少证明我们的多生产多消费模型是可以跑的。信号量另一个本质就是把临界资源是否存在就绪等的条件以原子性的形式呈现在访问临界资源之前就判断了经过对比上一篇的阻塞队列上一篇的 Equeue 是有判断的。所以我们通过环形队列就可以实现多资源分配了当 N1 时也就是转换成二元信号量和阻塞队列一致的效果都是访问一个整体资源了。所以信号量和互斥锁在管理不同类型资源时各有特点互斥锁主要用于保护单个共享资源确保同一时刻只有一个线程可以访问该资源。它是一种特殊的二进制信号量其计数器只有0和1两个值。互斥锁的操作通常包括加锁和解锁且必须由同一线程完成。它适用于需要严格互斥访问的场景如保护临界区或共享数据结构以防止数据竞争和不一致问题。信号量不仅可以用于互斥还可以用于控制对多个同类资源的访问允许多个线程同时访问资源但数量受限于信号量的计数器值。信号量的计数器可以是任意非负整数表示可用资源的数量。它适用于需要控制多个资源并发访问的场景如资源池管理或线程间的同步。总的来说互斥锁更适合管理单个资源的互斥访问而信号量则更适合管理多个资源的并发访问和线程同步。阻塞队列和环形队列都是队列为什么一个被当作整体资源整体看待一个被看成整体资源分块看待从底层实现的角度来看两个代码分别使用了std::queue和std::vector导致资源管理方式不同原因如下阻塞队列BlockQueue使用std::queue底层实现std::queue是一个容器适配器默认底层使用std::deque实现。std::deque是一个双端队列内部由多个小块连续空间组成整体逻辑上连续。整体资源看待的原因操作限制std::queue只提供了队列的基本操作如push、pop等无法直接访问队列中的元素只能通过队列的整体状态如是否为空、是否已满来控制操作。因此在阻塞队列中需要将整个队列作为一个整体资源进行保护以确保线程安全。线程同步阻塞队列通过条件变量_full_cond和_empty_cond来协调生产者和消费者。当队列满或空时需要对整个队列的状态进行判断和同步而不是单独操作队列中的某个元素。环形队列RingQueue使用std::vector底层实现std::vector是一个动态数组内部使用连续的内存空间存储元素。它提供了随机访问的能力并且可以在尾部高效地插入和删除元素。整体资源分块看待的原因随机访问与分块管理std::vector的连续内存空间使得可以通过索引直接访问任意元素。环形队列利用这一点将队列空间分为“空闲空间”和“已占用空间”两部分分别用信号量_blank_sem和_data_sem表示。生产者和消费者分别关注空闲空间和已占用空间而不是整个队列。独立控制信号量机制允许生产者和消费者独立操作。生产者在有空闲空间时通过_blank_sem判断进行生产消费者在有数据时通过_data_sem判断进行消费。这种独立性使得资源管理更加灵活避免了对整个队列加锁的开销。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2498744.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!