当前位置: 首页 > news >正文

线程同步与互斥(下)

线程同步与互斥(中)https://blog.csdn.net/Small_entreprene/article/details/147003513?fromshare=blogdetail&sharetype=blogdetail&sharerId=147003513&sharerefer=PC&sharesource=Small_entreprene&sharefrom=from_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 testusing 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::queue<T> _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)
{BlockQueue<task_t> *bq = static_cast<BlockQueue<task_t> *>(args);while (true){sleep(3);// 1. 消费任务task_t t = bq->Pop();// 2. 处理任务 -- 处理任务的时候,这个任务,已经被拿到线程的上下文中了,不属于队列了t();}
}void *productor(void *args)
{BlockQueue<task_t> *bq = static_cast<BlockQueue<task_t> *>(args);while (true){// 1. 获得任务std::cout << "生产了一个任务: " << std::endl;// 2. 生产任务bq->Equeue(Download);}
}int main()
{// 扩展认识: 阻塞队列: 可以放任务吗?// 申请阻塞队列BlockQueue<task_t> *bq = new BlockQueue<task_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?fromshare=blogdetail&sharetype=blogdetail&sharerId=146120541&sharerefer=PC&sharesource=Small_entreprene&sharefrom=from_link

POSIX是一种新的标准,和System V是类似的,只不过POSIX是更加常用的,反而是System V版本是快被淘汰了。

信号量也被称为信号灯,和信号根本没有联系,信号是一种进程间异步通知的方式,而信号量的本质是一个计数器,用来表示临界资源当中资源的数量有多少。对于放映厅的例子,我们只要将票买成功了,那么对应的位置就是属于我的了,哪怕今天我不去看这场电影了,放映厅这个位置也是必须要为我留的。

所以:信号量/信号灯本质就是一个计数器,是对待定资源的预定机制。

放映厅本身就是临界资源,我们看电影前买票,在技术人员看来就是保证放映厅内部资源的安全性,但是有一点点不一样,信号量是描述临界资源当中资源数量有多少:

假设放映厅中有50个座位,信号量的计数器就是50,申请一个,50--,当信号量被减到0时,就表明当前资源已经是被占用满的了,不能够再申请了。这样,我们就能够保证进到放映厅的人数不会超过座位数,最多50,然后再经过合理的调度,让不同的线程坐到不同的位置上,这样线程就可以访问放映厅的同时,访问放映厅内不同的座位了。我们说的这个放映厅的例子是将整个放映厅局部进行访问,也就是说,我们允许放映厅当中的座位被多个不同的人同时访问不同的位置,虽然整个放映厅是一个大的临界资源,但只要每一个人访问不同的小的资源,那么大家就不冲突。

那么如果有一个VIP放映厅,里面只有一个座位,所以对应的信号量计数器的值就是1,你只要申请成功的话,这个VIP放映厅就是你的,申请失败就要等,我们将这种计数器值为1的信号量,我们称之为二元信号量,二元信号量的本质不就是互斥嘛!幸运的是:我们目前已经学习了锁了😍

所以,多线程使用资源,有两种场景:

  • 将目标资源整体使用(使用锁计数/二元信号量)(上面我们按照条件变量所写的阻塞队列就是将阻塞队列这一资源整体使用的,所以我们要加锁)
  • 将目标资源按照不同的“块”,分批使用 (如果可以将一个整体资源划分成一块一块的局部资源,就可以往这些局部资源中放入线程,实现并发访问)

我们申请信号量,是要求所有的线程都得先看到信号量(sem),而信号量本身就是一个计数器,该计数器不再是我们所单纯认为的int count了(因为count++/--并不是原子的),所以我们可以对int count加一把锁,共同都成信号量,这样也说得通,但是我们知道信号量的实现肯定是更加复杂的。(申请资源 sem-- >>---<<释放资源 sem++ )。

所以,信号量本质也是临界资源,对应的P操作(就是对应信号量的--操作)一定是原子性的,V操作(就是对应信号量的++操作)一定是原子性的! 

基于环形队列的生产消费模型

环形队列是为了突出信号量的特征!!!

环形队列采用数组或链表模拟,用模运算来模拟环形状态!

  • 环形队列为空,代表head指针 == tail指针;
  • 环形队列为空,也是代表head指针 == tail指针;

我们应该清楚:写这种数据结构的时候,少不了对该数据结构进行判空或判满,很明显,空和满都是head == tail; 的,所以我们需要有空和满的解决方案:

  1. 方案1:使用计数器(int count,默认为0):如果环形队列入数据,那么相应的,count++; 出数据,count--; 所以count为0表示环形队列为空,count为环形队列的容量要求时,该环形队列为满;
  2. 方案2:预留一个空的位置,作满的状态:(下面是解释)(head == tail 默认表示的就是环形队列是空的)

将来生产(入队列)的时候,在不考虑消费(出队列)的过程的时候,tail的位置是处于内容为空的,也就是没有生产任务,但是是准备生产任务状态,是否生产任务,需要判断:tail的当前位置的下一个位置是否是head,不相等的话,就说明tail可以生产,因为没满。

if (head == tail)
{// 空状态
}
else if ((tail + 1) % n == head)
{// 满状
}
else
{// 可生产状态
}

我们环形队列是可以使用链表实现,但是没有必要,我们就按照数组模拟实现固定大小的环形队列(通过取模来实现位置的正确变换)

 对于我们上面的判空/判满的两种方案,其实对于今天的生产者消费者模型来说是没有用到的,因为我们还有另外一种方案:信号量!!!

我们形象一点,我们将环形队列看成一个大圆桌,每个位置只能放一个苹果(生产任务),为了实现生产者消费者模型,我们有如下几个约定:

  • 约定1: 队列为空,生产者先运行
  • 约定2: 队列为满,消费者先运行
  • 约定3: 生产者不能把消费者太一个圈以上(否者会将旧任务覆盖成新任务)
  • 约定4: 消费者不能超过生产者(否则根本就没有苹果给你消费) 

所以,遵守上面的约定:

  1. 生产者和消费者不访问同一个位置,两者就可以同时运行。
  2. 那么什么时候,两者会在同一位置呢?为空或者为满的时候!!!(也就是说,如果不为空或者不为满的时候,两者的操作就可以同时运行)

为空:只能【互斥】生产者先【同步】运行

为满:只能【互斥】消费者先【同步】运行

结论:(遵守4个约定下)

环形队列,不为空 && 不为满,生产消费可以同时运行;

环形队列,为空 || 为满的时候,生产和消费需要同步和互斥。



我们是使用信号量来保证上面的4个约定!!!信号量是表示临界资源数目的,所以:

对于生产者关心的资源:环形队列中空的位置(初始:sem_blank = N;)(N是环形队列的大小)

对于消费者关心的资源:环形队列中的有效数据(有数据)(初始:sem_data = 0;)

我们下面来模拟一下生产消费的追逐过程(伪代码形式):

//《《《《生产者》》》》
int p_step = 0; // 生产者起始位置下标
P(sem_blank);   // P操作: sem_blank--
p_step++;
p_step %= N;   // 走到下一个"空"位置
V(sem_data++); // 生产一个数据,当然就让消费数据量多了一个//《《《《消费者》》》》
int c_step = 0; // 消费者起始位置下标
P(sem_data);    // P操作: sem_data--(取走任务数据,也就是消费)
c_step++;
c_step %= N;    // 走到下一个"有效位置"位置
V(sem_blank++); // 消费一个数据,当然就让空位置多了一个

 信号量的P操作是原子的,申请成功,继续运行,申请失败,申请的线程会被阻塞(刚开始sem_data为0,sem_blank大于0,消费者就会在对应的P(sem_data)位置进行阻塞,生产者是可以进行P操作,这就是让队列为空的时候,生产者先运行)(生产满了也是如此)而且PV操作是原子的,所以如上的4个约定我们就可以遵守了。


POSIX版的信号量的接口介绍

初始化信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);

参数:

sem:指向信号量的指针。

pshared:0表示线程间共享,非零表示进程间共享。

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:信号量的初始值,默认为defaultvalueSem(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:队列容量,默认为gcapRingQueue(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::vector<T> _rq; // 环形队列的存储int _cap;           // 队列容量// 生产者相关Sem _blank_sem; // 空位置信号量int _p_step;    // 生产者步骤下标// 消费者相关Sem _data_sem; // 数据信号量int _c_step;   // 消费者步骤下标
};

这段代码定义了一个名为RingQueue的模板类,用于实现一个线程安全的环形队列。队列使用信号量和互斥锁来同步生产者和消费者之间的操作,确保了线程安全。生产者使用Equeue方法入队,消费者使用Pop方法出队。信号量用于控制空位置和数据的同步,互斥锁用于保护临界区,防止数据竞争。

我们这是基于单生产-单消费的模型:下面我们来测试一下:

Main.cc

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include "RingQueue.hpp"struct threaddata
{RingQueue<int> *rq;std::string name;
};void *consumer(void *args)
{threaddata *td = static_cast<threaddata *>(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_cast<threaddata *>(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()
{// 扩展认识: 阻塞队列: 可以放任务吗?// 申请阻塞队列RingQueue<int> *rq = new RingQueue<int>();// 构建生产和消费者// 单单: 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:队列容量,默认为gcapRingQueue(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::vector<T> _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
{RingQueue<int> *rq;std::string name;
};void *consumer(void *args)
{threaddata *td = static_cast<threaddata *>(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_cast<threaddata *>(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()
{// 扩展认识: 阻塞队列: 可以放任务吗?// 申请阻塞队列RingQueue<int> *rq = new RingQueue<int>();// 构建生产和消费者// 如果我们改成多生产多消费呢??// 单单: 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是有判断的)。

所以,我们通过环形队列就可以实现多资源分配了,当N=1时,也就是转换成二元信号量,和阻塞队列一致的效果,都是访问一个整体资源了。

所以:

对整体资源的使用: 使用互斥锁;

对整体资源中部分资源的使用:使用信号量。


阻塞队列和环形队列,都是"队列",为什么一个被当作整体资源整体看待,一个被看成整体资源分块看待? 

相关文章:

线程同步与互斥(下)

线程同步与互斥&#xff08;中&#xff09;https://blog.csdn.net/Small_entreprene/article/details/147003513?fromshareblogdetail&sharetypeblogdetail&sharerId147003513&sharereferPC&sharesourceSmall_entreprene&sharefromfrom_link我们学习了互斥…...

MySQL 优化教程:让你的数据库飞起来

文章目录 前言一、数据库设计优化1. 合理设计表结构2. 范式化与反范式化3. 合理使用索引 二、查询优化1. 避免使用 SELECT *2. 优化 WHERE 子句3. 优化 JOIN 操作 三、服务器配置优化1. 调整内存分配2. 调整并发参数3. 优化磁盘 I/O 四、监控与分析1. 使用 EXPLAIN 分析查询语句…...

SD + Contronet,扩散模型V1.5+约束条件后续优化:保存Canny边缘图,便于视觉理解——stable diffusion项目学习笔记

目录 前言 背景与需求 代码改进方案 运行过程&#xff1a; 1、Run​编辑 2、过程&#xff1a; 3、过程时间线&#xff1a; 4、最终效果展示&#xff1a; 总结与展望 前言 机器学习缺点之一&#xff1a;即不可解释性。最近&#xff0c;我在使用stable diffusion v1.5 Co…...

位掩码、哈希表、异或运算、杨辉三角、素数查找、前缀和

1、位掩码 对二进制数操作的方法&#xff0c;&#xff08;mask1<<n&#xff09;,将数mask的第n位置为1&#xff0c;其它位置为0&#xff0c;即1000...2^n&#xff0c;当n较小时&#xff0c;可以用于解决类似于0/1背包的问题&#xff0c;要么是0&#xff0c;要么是1&…...

安装OpenJDK1.8 17 (macos M芯片)

安装OpenJDK 1.8 下载完后&#xff0c;解压&#xff0c;打开 环境变量的配置文件即可 vim ~/.zshrc #export JAVA_HOME/Users/xxxxx/jdk-21.jdk/Contents/Home #export JAVA_HOME/Users/xxxxx/jdk-17.jdk/Contents/Home #export JAVA_HOME/Users/xxxxx/jdk-11.jdk/Contents…...

Spring Boot 自动加载流程详解

前言 Spring Boot 是一个基于约定优于配置理念的框架&#xff0c;它通过自动加载机制大大简化了开发者的配置工作。本文将深入探讨 Spring Boot 的自动加载流程&#xff0c;并结合源码和 Mermaid 图表进行详细解析。 一、Spring Boot 自动加载的核心机制 Spring Boot 的自动加…...

2025 年“认证杯”数学中国数学建模网络挑战赛 C题 化工厂生产流程的预测和控制

流水线上也有每个位置的温度、压力、流量等诸多参数。只有参数处于正常范 围时&#xff0c;最终的产物才是合格的。这些参数很容易受到外部随机因素的干扰&#xff0c;所 以需要实时调控。但由于参数众多&#xff0c;测量困难&#xff0c;很多参数想要及时调整并不容 易&#x…...

Richardson-Lucy (RL) 反卷积算法 —— 通过不断迭代更新图像估计值

文章目录 一、RL反卷积算法&#xff08;1&#xff09;主要特点&#xff08;2&#xff09;基本原理&#xff08;3&#xff09;关键步骤&#xff08;4&#xff09;优化算法 二、项目实战&#xff08;1&#xff09;RL 反卷积&#xff08;2&#xff09;优化&#xff1a;RL 反卷积 …...

2025.04.10-拼多多春招笔试第四题

📌 点击直达笔试专栏 👉《大厂笔试突围》 💻 春秋招笔试突围在线OJ 👉 笔试突围OJ 04. 优惠券最优分配问题 问题描述 LYA是一家电商平台的运营经理,负责促销活动的策划。现在平台上有 n n n...

------------------V2024-2信息收集完结------------------

第二部分信息收集完结撒花*★,*:.☆(&#xffe3;▽&#xffe3;)/$:*.★* 。 进入开发部分&#xff0c;工具要求&#xff1a;phpstorm Adobe Navicat16 小皮 准备完毕 php开发起飞起飞~~~~~...

Java Lambda与方法引用:函数式编程的颠覆性实践

在Java 8引入Lambda表达式和方法引用后&#xff0c;函数式编程范式彻底改变了Java开发者的编码习惯。本文将通过实战案例和深度性能分析&#xff0c;揭示如何在新项目中优雅运用这些特性&#xff0c;同时提供传统代码与函数式代码的对比优化方案。 文章目录 一、Lambda表达式&a…...

2025年常见渗透测试面试题- PHP考察(题目+回答)

网络安全领域各种资源&#xff0c;学习文档&#xff0c;以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具&#xff0c;欢迎关注。 PHP考察 php的LFI&#xff0c;本地包含漏洞原理是什么&#xff1f;写一段带有漏洞的代码。手工的话如何发掘&am…...

【在校课堂笔记】南山 - 第 10 节课 总结

- 第 92 篇 - Date: 2025 - 04 - 10 Author: 郑龙浩/仟墨 【Python 在校课堂笔记】 南山 - 第 10 节课 文章目录 南山 - 第 10 节课一 in –> 存在性测试 - 基础介绍二 in –> 例题 - 火车票 - 使用 in 优化**问题**【代码 - 以前的代码】【代码 - 使用存在性测试 in】 …...

GaussDB ECPG与Oracle Pro_C深度对比:嵌入式SQL开发者的迁移指南

GaussDB ECPG与Oracle Pro*C深度对比&#xff1a;嵌入式SQL开发者的迁移指南 一、体系架构差异 关键组件对比表 二、语法兼容性分析 核心语法差异对比 c /* Pro*C示例 */ EXEC SQL SELECT empno INTO :emp_id FROM employees WHERE ename :name;/* ECPG等效实现 */ EXEC…...

debian系统中文输入法失效解决

在 Debian 9.6 上无法切换中文输入法的问题通常与输入法框架&#xff08;如 Fcitx 或 IBus&#xff09;的配置或依赖缺失有关。以下是详细的解决步骤&#xff1a; 1. 安装中文语言包 确保系统已安装中文语言支持&#xff1a; sudo apt update sudo apt install locales sudo…...

2025年危化品安全管理人员备考指南|智能题库+核心考点解析

作为危化品生产单位安全管理人员&#xff08;主要负责人&#xff09;&#xff0c;考试内容主要涵盖三大模块&#xff1a; 法律法规体系 《安全生产法》修订要点&#xff08;2023版&#xff09; 危险化学品重大危险源辨识标准&#xff08;GB 18218&#xff09; 最新《化工过…...

我为女儿开发了一个游戏网站

大家好&#xff0c;我是星河。 自从协助妻子为女儿开发了算数射击游戏后&#xff0c;星河就一直有个想法&#xff1a;为女儿打造一个专属的学习游戏网站。之前的射击游戏虽然有趣&#xff0c;但缺乏难度分级&#xff0c;无法根据女儿的学习进度灵活调整。而且&#xff0c;仅仅…...

SpringBoot企业级开发之【用户模块-更新用户基本信息】

接口文档&#xff1a; 开发前我们先看一下接口文档&#xff1a; 这是实现的预想结果&#xff1a; 实现思路&#xff1a; 设计一下我们的实现思路 拿起家伙实操&#xff1a; 1.controller 定义一个方法去修改用户&#xff1a; 注意&#xff01;是【put请求】 //更改用户信…...

循环神经网络 - 长短期记忆网络的门控机制

长短期记忆网络&#xff08;LSTM&#xff09;的门控机制是其核心设计&#xff0c;用来解决普通 RNN 在长程依赖中遇到的梯度消失与信息混淆问题。为了更进一步理解长短期记忆网络&#xff0c;本文我们来深入分析一下其门控机制。 一、理解长短期记忆网络的“三个门” 所谓门控…...

AutoKeras 处理图像回归预测

AutoKeras 是一个自动机器学习库&#xff0c;在处理图像回归预测问题时&#xff0c;它可以自动选择最佳的模型架构和超参数&#xff0c;从而简化深度学习模型的构建过程。 AutoKeras 主要用于分类和回归任务&#xff0c;它同样可以进行图像数据的回归预测。 步骤 1: 安装 Auto…...

批量清空图片的相机参数、地理位置等敏感元数据

我们在使用相机或者手机拍摄照片的时候&#xff0c;照片中都会带有一些敏感元数据信息&#xff0c;比如说相机的型号&#xff0c;参数&#xff0c;拍摄的时间地点等等。这些信息虽说不是那么引人注意&#xff0c;但是在某些时候他是非常隐私非常重要的。如果我们将这些信息泄露…...

驱动-字符设备驱动框架

简要了解 字符设备驱动框架 整个流程 文章目录 基本知识&#xff1a;实际应用效果说明 参考资料字符设备驱动框架基本结构关键数据结构 - 文件操作结构体(file_operations)struct module *ownerssize_t (*read) (struct file *, char __user *, size_t, loff_t *);ssize_t (*wr…...

RK3588芯片NPU的使用:Windows11 Docker中运行PPOCRv4例子

本文的目标 本文将在RKNN Docker环境中练习PPOCR示例&#xff0c;并通过adb工具部署到RK3588开发板。 开发环境说明 主机系统&#xff1a;Windows11目标设备&#xff1a;搭载RK3588芯片的安卓开发板核心工具&#xff1a;包含rknn-toolkit2、rknn_model_zoo等工具的Docker镜像…...

88.高效写入文件—StringBuilder C#例子 WPF例子

在处理文件写入操作时&#xff0c;选择合适的方法可以显著影响程序的性能。本文将通过两个示例代码&#xff0c;对比使用 StringBuilder 和直接写入文件的性能差异&#xff0c;并提供具体的实现步骤。 问题背景 在实际开发中&#xff0c;我们经常需要将大量数据写入文件。然而…...

redis 延迟双删

Redis延迟双删是一种用于解决缓存与数据库数据一致性问题的策略&#xff0c;通常在高并发场景下使用。以下是其核心内容&#xff1a; 1. 问题背景 当更新数据库时&#xff0c;如果未及时删除或更新缓存&#xff0c;可能导致后续读请求仍从缓存中读取旧数据&#xff0c;造成数…...

如何在CentOS部署青龙面板并实现无公网IP远程访问本地面板

&#xfeff;青龙面板的功能多多&#xff0c;可以帮助我们自动化处理很多需要手动操作的事情&#xff0c;比如京东领京豆&#xff0c;阿里云盘签到白嫖 vip、掘金签到等等&#xff0c;本教程使用 Docker 搭建青龙面板&#xff0c;并结合 cpolar 内网穿透实现使用公网地址远程访…...

VectorBT量化入门系列:第五章 VectorBT性能评估与分析

VectorBT量化入门系列&#xff1a;第五章 VectorBT性能评估与分析 本教程专为中高级开发者设计&#xff0c;系统讲解VectorBT技术在量化交易中的应用。通过结合Tushare数据源和TA-Lib技术指标&#xff0c;深度探索策略开发、回测优化与风险评估的核心方法。从数据获取到策略部署…...

新能源商用车能耗终极优化指南:悬架、制动、电驱桥全链路硬核拆解(附仿真代码)

引言&#xff1a;新能源商用车的“续航战争”与工程师的破局点 1.1 行业现状&#xff1a;政策红利与技术瓶颈的博弈 数据冲击&#xff1a; 2023年中国新能源商用车销量突破50万辆&#xff0c;但平均续航仅为燃油车的55%&#xff08;数据来源&#xff1a;中汽协&#xff09;。…...

Maven笔记

Maven作用 依赖管理、版本控制标准化项目结构、自动化构建项目生命周期管理细分项目模块自动化构建、通过插件拓展构建过程 Maven下载及配置 https://blog.csdn.net/qq_29689343/article/details/135566775 使用IDEA 构建Maven工程 https://blog.csdn.net/qq_29689343/art…...

Java——接口扩展

JDK8开始接口中新增的方法 JDK7以前:接口中只能定义抽象方法。 JDK8的新特性:接口中可以定义有方法体的方法。(默认、静态) JDK9的新特性:接口中可以定义私有方法。 默认方法 需要使用关键字default修饰 作用: 解决接口升级的问题 接口中默认方法的定义格式: 格式: public d…...

COD任务论文--MAMIFNet

摘要 提示&#xff1a;论文机翻 由于难以从复杂背景中区分高度相似的目标&#xff0c;伪装物体检测&#xff08;COD&#xff09;仍然是计算机视觉领域的一项具有挑战性的任务。现有的伪装物体检测方法往往在场景理解和信息利用方面存在困难&#xff0c;导致精度不足&#xff0c…...

基于MCP协议调用的大模型agent开发04

目录 MCP客户端Client开发流程 uv工具 uv安装流程 uv的基本用法介绍 MCP极简客户端搭建流程 MCP客户端接入OpenAI、DeepSeek在线模型流程 参考来源及学习推荐&#xff1a; Anthropic MCP发布通告&#xff1a;https://www.anthropic.com/news/model-context-protocol MC…...

ComfyUI_Echomimic部署问题集合

本博客总结自己在从WebUI转到ComfyUI的过程配置Echomimic遇到的一些问题和解决方法。 默认大家已经成功安装ComfyUI&#xff0c;我之前装的是ComfyU桌面版&#xff0c;现在用的是B站秋葉大佬的整合包。但内核都一样&#xff0c;错误也是通用的。遇到问题时&#xff0c;应该先去…...

音频转文本:如何识别音频成文字

Python脚本:MP4转MP3并语音识别为中文 以下是一个完整的Python脚本,可以将MP4视频转换为MP3音频,然后使用语音识别模型将音频转换为中文文本。 准备工作 首先需要安装必要的库: pip install moviepy pydub SpeechRecognition openai-whisper完整脚本 import os from m…...

脑科学与人工智能的交叉:未来智能科技的前沿与机遇

引言 随着科技的迅猛发展&#xff0c;脑科学与人工智能&#xff08;AI&#xff09;这两个看似独立的领域正在发生深刻的交汇。脑机接口、神经网络模型、智能机器人等前沿技术&#xff0c;正带来一场跨学科的革命。这种结合不仅推动了科技进步&#xff0c;也在医疗、教育、娱乐等…...

Linux | I.MX6ULL外设功能验证(11)

01 CSI 摄像头测试 I.MX6ULL 终结者开发板引出了一路 CSI 的摄像头接口,支持【007】的 OV5640 摄像头模块。首先我们连接OV5640 摄像头模块到开发板上,如下图所示(大家在连接的时候一定要注意方向,摄像头朝向开发板的内侧,千万不要接反):...

AI助手:Claude

一、简介 Claude 是由 Anthropic 公司开发的一款人工智能助手&#xff0c;类似于 OpenAI 的 ChatGPT。它以 Anthropic 提出的“宪法式 AI&#xff08;Constitutional AI&#xff09;”为核心设计理念&#xff0c;强调安全性、透明性和可控性。以下是对 Claude 的一个简要介绍&…...

vue项目proxy代理的方式

以下是一个详细的 Vue 项目配置 Proxy 代理 的示例和说明&#xff0c;用于解决开发环境跨域问题&#xff1a; 1. 基础代理配置 vue.config.js 配置文件 // vue.config.js module.exports {devServer: {proxy: {// 代理所有以 /api 开头的请求/api: {target: http://localhos…...

多项目并行时如何避免资源冲突

多项目并行时避免资源冲突需做到&#xff1a;精确的资源规划与调度、建立统一的资源管理体系、设置清晰的优先级策略、实时监控资源使用状况、优化团队沟通与协调。其中&#xff0c;精确的资源规划与调度尤其重要&#xff0c;它决定了项目资源能否高效利用&#xff0c;防止资源…...

求x的c(n,m)次方

近期看到一类很有趣的题啊&#xff0c;其最基础的表现形式为求 mod P的值。 所以我们来拿一道小例题讲讲。 题面&#xff1a;给定 x,n,m&#xff0c;求&#xff1a; mod 1000003471的值。 首先我们注意到&#xff0c;题目给定的模数1000003471为质数&#xff0c;根据费马…...

VS Code 的 .S 汇编文件里面的注释不显示绿色

1. 确认文件语言模式 打开 .S 文件后&#xff0c;查看 VS Code 右下角的状态栏&#xff0c;确认当前文件的识别模式&#xff08;如 Assembly、Plain Text 等&#xff09;。如果显示为 Plain Text 或其他非汇编模式&#xff1a; 点击状态栏中的语言模式&#xff08;如 Plain Te…...

Apipost自定义函数深度实战:灵活处理参数值秘籍

在开发过程中&#xff0c;为了更好地处理传递给接口的参数值&#xff0c;解决在调试过程中的数据处理问题&#xff0c;我们经常需要用到函数处理数据。 过去&#xff0c;我们通过预执行脚本来处理数据&#xff0c;先添加脚本&#xff0c;然后将处理后的结果再赋值给请求参数。…...

ADI的BF561双核DSP怎么做开发,我来说一说(十)驱动直流电机和步进电机

作者的话 ADI的双核DSP&#xff0c;最早的一颗是Blackfin系列的BF561&#xff0c;这颗DSP我用了很久&#xff0c;比较熟悉&#xff0c;且写过一些给新手的教程。 硬件准备 ADZS-BF561-EZKIT开发板&#xff1a;ADI原厂评估板 AD-ICE20000仿真器&#xff1a;ADI现阶段性能最好…...

JS包装类型Object

包装类型 1 对象 Object 声明普通对象 学习静态方法&#xff0c;只能由Object自己调用 1.获得所有属性 2.获得所有属性值 3.对象拷贝...

【C++初阶】--- vector容器功能模拟实现

1.什么是vector&#xff1f; 在 C 里&#xff0c;std::vector 是标准模板库&#xff08;STL&#xff09;提供的一个非常实用的容器类&#xff0c;它可以看作是动态数组 2.成员变量 iterator _start;&#xff1a;指向 vector 中第一个元素的指针。 iterator _finish;&#x…...

FreeRTOS项目工程完善指南:STM32F103C8T6系列

FreeRTOS项目工程完善指南&#xff1a;STM32系列 本文是FreeRTOS STM32开发系列教程的一部分。我们将完善之前移植的FreeRTOS工程&#xff0c;添加串口功能并优化配置文件。 更多优质资源&#xff0c;请访问我的GitHub仓库&#xff1a;https://github.com/Despacito0o/FreeRTO…...

多值字典表设计:优雅处理一对多关系的数据库方案

在数据库设计中,我们经常需要处理一对多的关系数据。传统做法是创建关联表,但有时这种方式会显得过于复杂。今天,我将分享一种简单而实用的多值字典表设计方案,它适用于那些不需要对单个值进行复杂操作的场景。 为什么需要多值字典表? 在许多应用场景中,我们需要存储一…...

如何在Linux系统Docker部署Dashy并远程访问内网服务界面

&#xfeff;## 简介 Dashy 是一个开源的自托管的导航页配置服务&#xff0c;具有易于使用的可视化编辑器、状态检查、小工具和主题等功能。你可以将自己常用的一些网站聚合起来放在一起&#xff0c;形成自己的导航页。一款功能超强大&#xff0c;颜值爆表的可定制专属导航页工…...

GRBL运动控制算法(五)脉冲生成Bresenham算法

前言 在数控系统和运动控制领域&#xff0c;脉冲信号的精确生成是实现高精度位置控制的核心。GRBL作为一款高效、开源的嵌入式运动控制固件&#xff0c;其底层脉冲生成机制直接决定了步进电机的运动平滑性、响应速度及整体性能。而这一机制的核心&#xff0c;正是经典的Bresen…...

Java学习手册:Java发展历史与版本特性

Java作为全球最流行的编程语言之一&#xff0c;其发展历程不仅见证了技术的演进&#xff0c;也反映了软件开发模式的变革。从1995年的首次发布到如今的持续更新&#xff0c;Java始终保持着强大的生命力和广泛的影响力。本文将简要回顾Java的发展历程&#xff0c;并重点介绍其关…...