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

生产消费者模型 读写者模型

概念

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
请添加图片描述

如图就是通过一个容器来解决生产者和消费者之间的强耦合问题。
生产者和消费者之间不直接进行通讯,而是通过一个任务队列来进行通讯。比如,把任务队列比作一个超市,生产者比作是仓库的供货商,消费者就是顾客。仓库并不需要把生产好的商品卖给消费者,因为消费者的消费水平可能有限,而给超市的话一次可以供给非常多的商品;消费者也不需要到供货商那去消费,直接到超市消费就行了。即就是:

  • 生产者产生数据后不需要等待消费者消费,直接扔给任务队列;
  • 消费者消费数据的时候,直接去任务队列当中消费即可。
    所以任务队列就相当于我们之前的“管道”一样,平衡了生产者和消费者的处理能力。

基于阻塞队列的生产消费者模型

阻塞队列

在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
请添加图片描述

基本结构

通过名字就可以知道,我们先需要创建一个队列,当然队列的容量也不是无限的,当超过某个值的时候,就要进行阻塞,因此还需要一个变量来标识最大容量。这个队列可以放任何类型的数据,所以我们通过模板来进行调用。

template <class T>
class BlockQueue
{
public:private:queue<T> _q; // 共享资源int _maxcap; // 极值
};

接下来就要开始考虑线程之间的同步与互斥问题了。我们先来看几个问题

  1. 生产者与生产者之间可以同时访问共享资源吗?
    答:当然是不可以的。生产者和生产者之间相当于是竞争关系。比如说一个超市里面有100个货架,这100个货架只能摆放方便面,而“统一”和“白象”就是2个生产者,它们不能在同一时间摆放自己的产品,只能哪一家没有生产的时候,另一家才能摆放自己的产品。
  2. 消费者和消费者之间可以同时访问共享资源吗?
    答:也是不可以的。这也是属于竞争关系。比如说世界末日的时候,只剩下一袋方便面了,那么消费者和消费者之间不就要竞争这袋方便面了吗?
  3. 消费者和生产者之间可以同时访问共享资源吗?
    答:也是不可以的。生产者和消费者之间首先要保证数据安全,只允许一个人去访问这个资源。比如说你今天去超市买方便面,但是方便面卖完了并且供货商也放假了,第二天你又去超市买方便面还是没有,你连续问了一个月,超市一直都说没有,所以这不仅浪费了自己的时间,也浪费了超市的时间,因为可能不止你一个消费者来问。同理,供货商来问超市需不需要方便面,超市说几乎没有客人,让供货商等等;每天供货商都来问超市,而超市给的答复都是一样的,同样都浪费了2者的时间。这2种方式都没有错,都保证了数据的安全性。但是并不合理!假如你去超市买方便面,但超市暂时没有,超市把你电话留下来等有了再告诉你,然后你再来,对供货商也是一样的做法,这样不就能让供货商和消费者协同起来了嘛,生产一部分消费一部分。
    综上所述,得到的结论是:
  • 生产者和生产者之间是互斥关系;
  • 消费者和消费者之间是互斥关系;
  • 生产者和消费者之间是同步和互斥关系。

生产消费者模型只需要记住“321原则”就行。3种关系,2个角色(生产者和消费者),1个共享资源(特定的共享缓冲区)。
那么需要几把锁来完成这个任务呢?其实只需要一把就行,因为都是互斥关系,所有线程访问阻塞队列时都需要互斥。
还有什么时候生产者可以把数据放入阻塞队列,消费者什么时候可以把数据从阻塞队列中拿出来呢?答案就是当队列中还有剩余空间时,生产者可以生产数据;当队列中还有数据时,消费者可以消费数据。
还有一点是生产者和消费者不可以共用一个条件变量,如果共用条件变量的话,当唤醒等待队列当中的线程的时候,不知道唤醒的是消费者还是生产者。这样的话,我们就需要定义2个条件变量了。

template <class T>
class BlockQueue
{
public:private:queue<T> _q; // 共享资源int _maxcap; // 极值pthread_cond_t _c_cond;  // 消费者条件变量pthread_cond_t _p_cond;  // 消费者条件变量pthread_mutex_t _mutex;  // 互斥锁
};

接下来我们就需要写构造和析构函数了。
构造函数

static const int defaultnum = 5;
BlockQueue(int maxcap = defaultnum): _maxcap(maxcap){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_c_cond, nullptr);pthread_cond_init(&_p_cond, nullptr);}

我们需要先指定队列的上限,设置为5,。然后初始化锁和条件变量。
析构函数

~BlockQueue(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_c_cond);pthread_cond_destroy(&_p_cond);}

准备工作完了之后,接下来就正式开始完成生产消费者模型了。

生成数据

我们可以使用队列的push接口来往阻塞队列中放数据。我们还要考虑线程的安全问题:

  1. 访问阻塞队列前先加锁,因为判断的时候必须访问临界资源。
  2. 阻塞队列满了的话,生产者就要等待;没有满的话,就可以往阻塞队列中放数据了。
  3. 放入数据完成之后,唤醒消费者来消费。

代码:

void push(const T &in)
{pthread_mutex_lock(&_mutex);// 为什么判断要放在加锁之后呢?因为判断的时候必须访问临界资源while (_maxcap == _q.size()) // 确保生成条件满足{pthread_cond_wait(&_p_cond, &_mutex); // 调用的时候,自动释放锁}_q.push(in);pthread_cond_signal(&_c_cond); // 只要生产了,通知消费者来消费pthread_mutex_unlock(&_mutex);
}

消费数据

消费者需要到队列中获取数据,我们可以使用队列的pop接口。步骤和刚才的类似:

  1. 访问阻塞队列前先加锁,因为判断的时候必须访问临界资源。
  2. 如果阻塞队列为空的话,消费者就要进行等待;不为空的话,就可以进行消费数据了。
  3. 消费完成之后,就要唤醒生产者来生成数据。消费一个生产一个。

代码:

T pop()
{pthread_mutex_lock(&_mutex);while (_q.size() == 0){pthread_cond_wait(&_c_cond, &_mutex);}T out = _q.front();_q.pop();pthread_cond_signal(&_p_cond); // 只要消费了,通知生产者来生成pthread_mutex_unlock(&_mutex);return out;
}

把模型写完之后,我们就要来测试代码了,需要写一个主函数来进行测试。如下:
main.cc

#include "BlockQueue.hpp"
#include <unistd.h>void *Consumer(void *args)
{BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);// 消费while (true){sleep(2);int data = bq->pop();cout << "消费了一个数据:" << data << endl;}return nullptr;
}void *Productor(void *args)
{BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);int data = 0;// 生产while (true){data++;bq->push(data);cout << "生产了一个数据:" << data << endl;// sleep(2);}return nullptr;
}int main()
{BlockQueue<int> *bq = new BlockQueue<int>();pthread_t c, p;pthread_create(&c, nullptr, Consumer, bq);pthread_create(&p, nullptr, Productor, bq);pthread_join(c, nullptr);pthread_join(p, nullptr);delete bq;return 0;
}

BlockQueue.hpp

#pragma once
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <queue>using namespace std;template <class T>
class BlockQueue
{static const int defaultnum = 5;
public:BlockQueue(int maxcap = defaultnum): _maxcap(maxcap){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_c_cond, nullptr);pthread_cond_init(&_p_cond, nullptr);_low_water = _maxcap / 3;        // 剩余空间只要低于这个就通知生产者生产_high_water = (_maxcap * 2) / 3; // 剩余数据只要高于这个就通知消费者消费}// 消费数据T pop(){pthread_mutex_lock(&_mutex);while (_q.size() == 0){pthread_cond_wait(&_c_cond, &_mutex);}T out = _q.front();_q.pop();pthread_cond_signal(&_p_cond); // 只要消费了,通知生产者来生成pthread_mutex_unlock(&_mutex);return out;}// 生成数据void push(const T &in){pthread_mutex_lock(&_mutex);// 为什么判断要放在加锁之后呢?因为判断的时候必须访问临界资源while (_maxcap == _q.size()) // 确保生成条件满足{pthread_cond_wait(&_p_cond, &_mutex); // 调用的时候,自动释放锁}_q.push(in);pthread_cond_signal(&_c_cond); // 只要生产了,通知消费者来消费pthread_mutex_unlock(&_mutex);}~BlockQueue(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_c_cond);pthread_cond_destroy(&_p_cond);}
private:queue<T> _q; // 共享资源int _maxcap; // 极值pthread_cond_t _c_cond;  // 消费者条件变量pthread_cond_t _p_cond;  // 消费者条件变量pthread_mutex_t _mutex;  // 互斥锁
};

我们先让消费者休眠2秒,在这2秒之间,生产者没有休眠而是一直在产生数据。来看一下结果。
请添加图片描述

可以看到,消费者开始消费的时候,消费的是之前的数据,而生产者一直在产生新数据。


接着,我们让生产者先休眠2秒,消费者不休眠,看看结果。
请添加图片描述

可以看到是产生一个数据,消费一个数据。


这回让消费者和生产者都不休眠。
请添加图片描述

环形队列的生产消费者模型

POSIX信号量

信号量的本质
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。
信号量本质上是一个非负整数计数器,用于控制对共享资源的访问。它可以用来实现互斥(确保同一时间只有一个进程或线程访问共享资源)和同步(协调多个进程或线程的执行顺序)。这个计数器用于表示某种资源的可用数量或者是某种事件已经发生的次数等。例如,在一个简单的多线程环境中,信号量的值可以代表可用的共享缓冲区的数量。如果信号量初始值为 5,就意味着有 5 个缓冲区可供使用。
它是一种同步原语,用于协调多个进程或线程对共享资源的访问。进程或线程在访问共享资源之前,需要先检查信号量的值来确定是否能够获取资源。


二元信号量和计数信号量

  • 二元信号量(互斥信号量):这是信号量的一种特殊情况,其值只能是 0 或者 1。它主要用于实现互斥访问,确保在同一时刻只有一个进程或线程能够访问共享资源。比如,在一个文件写入的场景中,为了避免多个进程同时写入导致文件内容混乱,使用二元信号量来控制对文件的访问权限。当一个进程想要写入文件时,它首先会尝试获取信号量(如果信号量的值为 1,获取成功后信号量的值变为 0),写完后再释放信号量(将信号量的值变回 1)。
  • 计数信号量:其值可以是大于等于 0 的整数。计数信号量用于管理多个相同类型的资源。例如,在一个网络服务器中,有多个连接套接字资源,信号量的初始值可以设置为套接字的总数。当有一个新的客户端连接请求到来时,服务器进程会尝试获取一个信号量(信号量的值减 1),如果信号量的值大于等于 0,表示还有可用的套接字资源,就可以为客户端分配一个套接字;当客户端断开连接后,服务器进程会释放信号量(信号量的值加 1)。

等待(P 操作)和发布(V 操作)

  • 等待(P 操作):在经典的信号量操作中,等待操作也称为 P 操作。当一个进程或线程执行 P 操作时,它会检查信号量的值。如果信号量的值大于 0,那么信号量的值减 1,进程或线程可以继续执行后续操作,这表示它成功获取了一个资源。如果信号量的值等于 0,那么执行 P 操作的进程或线程会被阻塞,进入等待状态,直到信号量的值大于 0,它才会被唤醒并执行信号量减 1 的操作。例如,在一个打印机资源共享的场景中,当一个用户(进程或线程代表)想要打印文档时,它执行 P 操作,如果打印机空闲(信号量值大于 0),信号量值减 1,用户可以使用打印机;如果打印机正在被使用(信号量值为 0),用户就会等待。
  • 发布(V 操作):发布操作也称为 V 操作。当一个进程或线程执行 V 操作时,它会将信号量的值加 1。如果有其他进程或线程因为执行 P 操作而被阻塞等待这个信号量,那么其中一个等待的进程或线程会被唤醒,尝试再次执行 P 操作获取资源。例如,在打印机的场景中,当一个用户打印完文档后,执行 V 操作,信号量的值加 1,如果有其他用户在等待使用打印机,就会有一个用户被唤醒并可以使用打印机。

接口

信号量的类型是sem_t,所有对信号量的操作,都是基于一个sem_t类型的变量。头文件是<semaphore.h>

sem_init
函数原型:int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:初始化一个信号量。
参数:对于无名信号量,使用sem_init函数进行初始化。它接受三个参数,信号量指针、共享标志(用于指定信号量是在进程间还是线程间共享,0 表示线程间共享,非 0 表示进程间共享)和初始值。
返回值:成功返回0,失败返回-1。


sem_destroy
函数原型:int sem_destroy(sem_t *sem);
功能:用于销毁一个信号量。
返回值:成功返回0,失败返回-1。


sem_wait
函数原型:int sem_wait(sem_t *sem);
功能:等待信号量,会将信号量的值减1。


sem_post
函数原型:int sem_post(sem_t *sem);
功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。

环形队列

环形队列采用数组模拟,用模运算来模拟环状特性,我们使用信号量来完成生产消费者模型。还是先用单消费单生产来完成。
请添加图片描述

最开始它们指向同一个位置,生产者在放数据,消费者在拿数据。对于生产者和消费者来说,它们在什么情况下会处于同一个位置呢?答案就是当队列为空或者队列为满的时候。其它情况下,它们就处于不同的位置。
下面,我们来玩一个游戏帮助理解环形队列的运行逻辑。
请添加图片描述

如图所示,在一张桌子上放满了9个盘子,刚开始我们站在同一个盘子前。游戏规则是,我不断的向盘子中放一个水果,放完之后就往下一个盘子继续放,你必须从盘子中拿起水果,但是你觉得这个水果不好吃,然后你继续追我。我只能往盘子中放一个水果,你不能跳过盘子拿自己喜欢的水果。我们在玩游戏的时候,应该怎样保证游戏的正常运行呢?需要遵循以下几个原则。

  1. 你不能超过我;
  2. 我不能套你一圈;
  3. 指向同一个位置时,只能一个人访问

那我们什么时候会站在一起呢?只能是当所有的盘子为空或者所有的盘子都有水果的时候才会在一起。
当盘子全为空的时候谁先运行呢?当然是生产者,也就是我先运行,不然你怎么拿水果。
当盘子都是水果的时候谁先运行呢?当然是消费者,也就是你先运行,不然我往哪放水果。
综上所述,我们得出一个结论:在环形队列当中,大部分情况下,单生产者和单消费者是可以并发执行的,只有队列为满或者为空时,才会出现互斥和同步的问题。
其实对于生产者来说,看重的是队列中还有没有空间;对于消费者来说,看重的是队列中有没有数据。所以为了更好的衡量这两者的关系,我们给空间资源定义一个信号量,给数据资源定义一个信号量。
那么我们该如何使用信号量来完成环形队列的生产消费者模型呢?我们先来看一下信号量的定义。

struct semaphore
{int value;struct PCB* queue;
}

P原语所执行的操作可用如下函数wait(s)来表示。

void wait(semaphore s)
{s.value = s.value-1;if(s.value < 0)block(s.queue);  // 将进程阻塞,并将其投入等待队列s.queue
}

V原语所执行的操作可用下面的函数signal(s)来表示。

void signal(semaphore s)
{s.value = s.value + 1;if(s.value < 0)wakeup(s.queue); // 唤醒阻塞进程,将其从等待队列s.queue取出。投入就绪队列
}

信号量的物理意义:

  1. 在信号量机制中,信号量的初值s.value表示系统中某种资源的数目,因而又称为资源信号量。
  2. P操作意味着进程请求一个资源,因此描述为 s.value=s.value-1;当 s.value<0时,表示资源已经分配完毕,因而进程所申请的资源不能够满足,进程无法继续执行,所以进醒执行block(s.queue)自我阻塞,放弃处理机,并插人等待该信号量的等待队列。
  3. V操作意味着进程释放一个资源,因此描述为s.value=s.value+1;当 s.value<0时,表示在该信号量的等待队列中有等待该资源的进程被阻塞,故应调用 wakeup(s.queue)原语将等待队列中的一个进程唤醒。
  4. s.value<0时,|s.value|表示等待队列的进程数。

基本结构
我们用vector来模拟环形队列,因为我们需要对队列进行随机访问。我们还有要以下几个基本变量:

  • _cap:队列的最大容量。
  • _c_step:当前消费者访问哪一个数据块,也就是消费者的下标。
  • _p_step:当前生产者访问哪一个数据块,也就是生产者的下标。
    还有就是生产者和消费者所关注的资源
  • _cdata_sem:消费者关注的数据资源。
  • _pspace_sem:生产者关注的空间资源。
    还有两把锁:
  • _c_mutex:消费者和消费者之间的互斥锁。
  • _p_mutex:生产者和生产者之间的互斥锁。

代码:

template<class T>
class RingQueue
{
private:vector<T> _ringqueue;int _cap;int _c_step;  // 消费者下标int _p_step;  // 生产者下标sem_t _cdata_sem;  // 消费者关注的数据资源sem_t _pspace_sem;  // 生产者关注的空间资源pthread_mutex_t _c_mutex;pthread_mutex_t _p_mutex;
};

构造和析构
构造函数中,要先指定队列的大小,然后初始化信号量和互斥锁。析构函数就是销毁信号量和锁。

RingQueue(int cap = defalutcap):_ringqueue(cap),_cap(cap),_c_step(0),_p_step(0)
{sem_init(&_cdata_sem,0,0);  // 第2个参数为0,表示是线程共享sem_init(&_pspace_sem,0,cap);pthread_mutex_init(&_c_mutex,nullptr);pthread_mutex_init(&_p_mutex,nullptr);
}~RingQueue()
{sem_destroy(&_cdata_sem);sem_destroy(&_pspace_sem);pthread_mutex_destroy(&_c_mutex);pthread_mutex_destroy(&_p_mutex);
}

生成数据
在此之前,我们先把P操作、V操作、加锁和解锁写成接口的形式。

void P(sem_t &sem)   //P是等待
{sem_wait(&sem)
}void V(sem_t &sem)  // V是发布
{sem_post(&sem);
}void Lock(pthread_mutex_t &mutex)
{pthread_mutex_lock(&mutex);
}void UnLock(pthread_mutex_t &mutex)
{pthread_mutex_unlock(&mutex);
}

我们生成数据的过程是先竞争信号量,再申请锁,释放锁,最后在放数据。

void Push(const T& in)  // 生产
{P(_pspace_sem);Lock(_p_mutex);_ringqueue[_p_step] = in;// 位置后移,保持环状特性_p_step++;_p_step %= _cap;UnLock(_p_mutex);V(_cdata_sem);
}

先用P操作让生产者申请一个信号量_pspace_sem,如果信号量的值大于 0,那么信号量的值减 1,线程可以继续执行后续操作,这表示它成功获取了一个资源。如果信号量的值等于 0,那么执行 P 操作的线程会被阻塞,进入等待状态,直到信号量的值大于0,它才会被唤醒。随后,就是加锁,与其它的生产者保持互斥的状态,再把数据投放给环形队列,_p_step %= _cap;是为让数组具有环的特性。最后再执行V操作锁,让信号量的值加1,相当于队列中多了一个数据。


消费数据

void Pop(T* out)  // 消费
{P(_cdata_sem);Lock(_c_mutex);*out = _ringqueue[_c_step];_c_step++;_c_step %= _cap;UnLock(_c_mutex);V(_pspace_sem);
}

跟上面的一样,首先消费者申请一个信号量_cdata_sem,相当于让队列中的数据减1,然后进行加锁,与其它消费者保持互斥,进入临界区后,把生产者生产的数据交给out变量,接着++和取模操作是为了让消费者走到下一个位置上和保持环状特性,最后再执行V操作,相当于_pspace_sem++,表示队列当中多了一个空位置。


完整代码:
RingQueue.hpp

#pragma once
#include <iostream>
#include <vector>
#include <semaphore.h>
#include <pthread.h>
using namespace std;const static int defalutcap = 5;template <class T>
class RingQueue
{
private:void P(sem_t &sem) // P是等待{sem_wait(&sem);}void V(sem_t &sem) // V是发布{sem_post(&sem);}void Lock(pthread_mutex_t &mutex){pthread_mutex_lock(&mutex);}void UnLock(pthread_mutex_t &mutex){pthread_mutex_unlock(&mutex);}
public:RingQueue(int cap = defalutcap): _ringqueue(cap), _cap(cap), _c_step(0), _p_step(0){sem_init(&_cdata_sem, 0, 0); // 第2个参数为0,表示是线程共享sem_init(&_pspace_sem, 0, cap);pthread_mutex_init(&_c_mutex, nullptr);pthread_mutex_init(&_p_mutex, nullptr);}void Push(const T &in) // 生产{P(_pspace_sem);Lock(_p_mutex);_ringqueue[_p_step] = in;// 位置后移,保持环状特性_p_step++;_p_step %= _cap;UnLock(_p_mutex);V(_cdata_sem);}void Pop(T *out) // 消费{P(_cdata_sem);Lock(_c_mutex);*out = _ringqueue[_c_step];_c_step++;_c_step %= _cap;UnLock(_c_mutex);V(_pspace_sem);}~RingQueue(){sem_destroy(&_cdata_sem);sem_destroy(&_pspace_sem);pthread_mutex_destroy(&_c_mutex);pthread_mutex_destroy(&_p_mutex);}
private:vector<T> _ringqueue;int _cap;int _c_step; // 消费者下标int _p_step; // 生产者下标sem_t _cdata_sem;  // 消费者关注的数据资源sem_t _pspace_sem; // 生产者关注的空间资源pthread_mutex_t _c_mutex;pthread_mutex_t _p_mutex;
};

其它常见的锁

  • 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
  • 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
  • CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
  • 自旋锁,公平锁,非公平锁。

读者写者模型

读写锁

在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。
请添加图片描述

  • 注意:写独占,读共享,读锁优先级高

“读者” 线程主要是读取共享资源中的数据,“写者” 线程则是修改共享资源中的数据。
在读者——写者问题中,任何时刻要求写者最多只允许有一个,读者可以有很多,因为写者要改变数据对象的内容,如果它们同时操作,则数据对象的内容则会变得不可知。
我们先讲一个故事来理解。
我们都要通过写博客来帮助自己理解并加深所学的内容,博客的作者就是写者,看你博客的同学叫做读者。比如说当你写博客的时候,你的同学在旁边看你正在写的内容,当你写了一部分的时候,同学1说你正在写C语言部分的知识点,同学2说你写的是C++部分的知识点,当你写完之后,才看出来你写的是C和C++不同处的知识点,在这里其他人读到的数据都是内容的局部东西,都猜的不对,拿的数据都不正确。所以你为了保证其它人能够正确的读到完整的内容,你给其他人出了一个规定:当你写博客的时候,其他人不能在旁边看,要么你就不写,要么就是写完博客才允许其它人看。这里写好的博客并不会规定读者排好队一个一个的进行观看,其他人读完才能轮到下一个人观看,这是不对的;还有就是写博客的时候,并不会要求只能一个人写,其它写者也可以参与进来,但是当一个人在写的时候不会让另一个人也参与进来编写,还是要让写者一个一个的写。
故事讲完了,分析读者写者问题思路还是遵循生产者消费者模型的321原则。写博客的作者就是写者,看博客的人就是读者。
至此,我们可以来分析一下同步与互斥的关系:

  • 写者和写者:是互斥关系。一个人写的时候,其他人不能写。
  • 读者和读者:即不互斥,也不同步。读者可以同时观看你写的内容,彼此之间没有影响。
  • 写者和读者:互斥和同步。写者正在写的时候,读者不能读,不然读到的数据就是不完整的,这就是互斥;你写完博客之后,要是没人看,那么你写的博客就没有意义,同样,要是发现你的博客数据有点不对,需要让写者更新数据,这就是同步关系。

读者写者模型和生产消费者模型之间的本质区别是什么?
写者和生产者一样,主要就是读者和消费者的区别,消费者可以消费你的数据,但是读者并不会修改你的数据,因为读者之间可以同时访问。
读者写者有2种不同实现的策略。

  • 读者优先策略:只要有一个读者在访问共享资源,写者就不能访问,直到所有读者都完成访问。这种策略优先考虑读者的并发访问。
  • 写者优先策略:写者一旦请求访问共享资源,就会阻止后续的读者和写者访问,直到写者完成操作。这种策略优先保证写者对资源的独占访问。

应用场景:

  • 数据库系统:在数据库管理系统中,当多个用户(读者)查询数据库中的数据时,这些查询操作可以同时进行。但是,当一个用户(写者)执行插入、更新或删除操作时,需要对相应的数据表进行排他性的访问,以防止数据不一致。例如,在一个在线购物网站的数据库中,多个顾客可以同时查看商品信息(读者操作),但当库存管理员更新商品库存(写者操作)时,需要保证数据的准确性,防止其他操作干扰。
  • 文件系统访问:多个进程可能需要读取一个文件的内容(读者),而在文件被修改(写者)时,需要保证修改操作的独占性。例如,在一个多人协作的文档编辑场景中,多个用户可以同时查看文档(读者),但当一个用户进行编辑(写者)时,需要阻止其他用户同时编辑和查看正在编辑中的部分,以避免出现混乱。

接口介绍
读写锁的数据类型是pthread_rwlock_t,使用方式与互斥锁类似,包含在<pthread.h>头文件中,它与互斥锁一样,可以全部和局部进行定义。
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
初始化
函数原型:int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);


销毁
函数原型:int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);


加锁和解锁
读者加锁:int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
写者加锁:int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
解锁:int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
解锁可以同时释放读者和写者的锁。
接下来我们通过伪代码来理解rwlock的实现原理。
请添加图片描述

该伪代码的意思是先给读者加锁,如果读者进来了,那么执行if语句,写者就无法进入,只能阻塞等待;读取数据的时候,写者不能写入,当没有读者的时候,那就释放写者。
在读者写者模型中,大多数的情况都是读者多,写者少,这样会很容易造成写者饥饿的问题。 比如说,有10和读者和1个写者,这10个读者都会不断的访问共享资源,使得写者长时间无法获取对共享资源的访问权,从而导致写者被 “饿死”,即写者的请求一直被推迟或无法执行。所以这个模型就是天然具备写者“饥饿”问题,默认就是读者优先。要想解决“饥饿”问题,可以用以下几种方法:

  • 公平策略:可以使用一个先进先出(FIFO)的队列来管理访问请求,无论是读者还是写者的请求,都按照它们到达的顺序排队。这样可以避免写者因为读者的频繁访问而一直无法获取资源。
  • 优先级调整:给写者赋予一定的优先级。当写者等待时间过长时,可以适当提高其优先级,使得写者能够在一定程度上优先于读者获取资源。不过,这种方法需要谨慎使用,因为过度提高写者优先级可能会导致读者饥饿。
  • 时间片机制:为读者和写者分配时间片。例如,规定读者在一个时间片内可以连续访问共享资源,时间片结束后,如果有写者等待,就把资源访问权交给写者。通过这种方式来平衡读者和写者的访问,避免写者长期无法访问的情况。

相关文章:

生产消费者模型 读写者模型

概念 生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯&#xff0c;而通过阻塞队列来进行通讯&#xff0c;所以生产者生产完数据之后不用等待消费者处理&#xff0c;直接扔给阻塞队列&#xff0c;消费者不找生产者要数据…...

每日Prompt:双重曝光

提示词 双重曝光&#xff0c;Midjourney 风格&#xff0c;融合、混合、叠加的双重曝光图像&#xff0c;双重曝光风格。一幅由 Yukisakura 创作的杰出杰作&#xff0c;展现了一个奇妙的双重曝光构图&#xff0c;将阿拉贡阿拉松之子的剪影与生机勃勃春季里中土世界视觉上引人注目…...

基于springboot3 VUE3 火车订票系统前后端分离项目适合新手学习的项目包含 智能客服 换乘算法

​ 博主介绍&#xff1a;专注于Java&#xff08;springboot ssm 等开发框架&#xff09; vue .net php phython node.js uniapp 微信小程序 等诸多技术领域和毕业项目实战、企业信息化系统建设&#xff0c;从业十五余年开发设计教学工作 ☆☆☆ 精彩专栏推荐订阅☆☆☆☆☆…...

DAY29 超大力王爱学Python

知识点回顾 类的装饰器装饰器思想的进一步理解&#xff1a;外部修改、动态类方法的定义&#xff1a;内部定义和外部定义 作业&#xff1a;复习类和函数的知识点&#xff0c;写下自己过去29天的学习心得&#xff0c;如对函数和类的理解&#xff0c;对python这门工具的理解等&…...

jvm对象压缩

最近在看一些文章&#xff0c;知道64位jvm在启动时指定-XX:UseCompressedOops后就会开启压缩&#xff0c;但是怎么压缩的&#xff0c;以及什么情况下压缩失效&#xff0c;没有一篇文章讲的特别透彻&#xff0c;在这里记录一下&#xff0c;后面抽时间进行更新。 参考文档 https…...

TripGenie:畅游济南旅行规划助手:个人工作纪实(十八)

本周&#xff0c;我增加了网页搜索并在右侧显示搜索链接与标题的功能&#xff0c;通过这个功能&#xff0c;可以帮助用户搜索网络上关于济南旅游的信息&#xff0c;提高用户检索信息的速度&#xff0c;用户可以通过点击网页链接了解更多关于济南的信息。 首先&#xff0c;我设计…...

Brooks Polycold快速循环水蒸气冷冻泵客户使用手含电路图,适用于真空室应用

Brooks Polycold快速循环水蒸气冷冻泵客户使用手含电路图&#xff0c;适用于真空室应用...

Rofin PowerLine E Air维护和集成手侧激光Maintenance and Integration Manual

Rofin PowerLine E Air维护和集成手侧激光Maintenance and Integration Manual...

C++中String类

1.String学习前的了解 1.1范围for和auto关键字 范围 for 循环 范围 for 循环是 C11 引入的一种语法糖&#xff0c;用于简化遍历容器或序列的代码。其基本语法如下&#xff1a; 迭代器版本&#xff1a; 范围for主要用来遍历一个容器(数据结构).范围for的&#xff08;&#xf…...

LSTM语言模型验证代码

#任务&#xff1a;基于已有文本数据&#xff0c;建立LSTM模型&#xff0c;预测序列文字 1 完成数据预处理&#xff0c;将文字序列数据转化为可用于LSTM输入的数据。 2 查看文字数据预处理后的数据结构&#xff0c;并进行数据分离操作 3 针对字符串输入&#xff08;“In the hea…...

Nextjs App Router 开发指南

Next.js是一个用于构建全栈web应用的React框架。App Router 是 nextjs 的基于文件系统的路由器&#xff0c;它使用了React的最新特性&#xff0c;比如 Server Components, Suspense, 和 Server Functions。 术语 树(Tree): 一种用于可视化的层次结构。例如&#xff0c;包含父…...

测试W5500的第3步_使用ioLibrary库创建TCPServer

W5500是一款具有8个Socket的网络芯片&#xff0c;支持TCP Server模式&#xff0c;最多可同时连接8个客户端。本文介绍了基于STM32F10x和W5500的TCP Server实现&#xff0c;包括SPI初始化、W5500复位、网络参数配置、Socket状态管理等功能&#xff0c;适用于需要多客户端连接的嵌…...

Python训练营打卡——DAY31(2025.5.20)

目录 一、机器学习项目的流程 二、文件的组织 1. 项目核心代码组织 2. 配置文件管理 3. 实验与探索代码 4. 项目产出物管理 三、注意事项 if name "main" 编码格式 类型注解 四、通俗解释 1. 拆分文件和目录&#xff1a;整理房间一样管理代码​​ 2. 导…...

P1152 欢乐的跳

P1152 欢乐的跳 - 洛谷 使用map映射来解 #include<bits/stdc.h> using namespace std; int n,a[1005],c[1005]; map<int,int>b;//因为要记录1~n-1是否都出现了 int main(){cin>>n;cin>>a[1];for(int i2;i<n;i){cin>>a[i];//c[i-1]a[i]-a[…...

基于 STM32 的蔬菜智能育苗系统硬件与软件设计

一、系统总体架构 蔬菜智能育苗系统通过单片机实时采集温湿度、光照等环境数据,根据预设阈值自动控制灌溉、补光、通风等设备,实现育苗环境的智能化管理。系统主要包括以下部分: 主控芯片:STM32F103C8T6(32 位 ARM Cortex-M3 单片机,性价比高,适合嵌入式控制)传感器模…...

数论:数学王国的密码学

在计算机科学的世界里&#xff0c;数论就像是一把神奇的钥匙&#xff0c;能够解开密码学、算法优化、随机数生成等诸多领域的谜题。作为 C 算法小白&#xff0c;今天我就带大家一起走进数论的奇妙世界&#xff0c;探索其中的奥秘。 什么是数论&#xff1f; 数论是纯粹数学的分…...

软考软件评测师——黑盒测试测试方法

以下为优化后的博客内容&#xff1a; 软件测试方法论精要 第一部分 核心知识点解析 一、等价类划分法 基本概念 将测试对象的输入域划分为若干子集&#xff0c;每个子集选取代表性样本作为测试用例。分为有效等价类&#xff08;合法输入&#xff09;和无效等价类&#xff0…...

python训练 60天挑战-day31

知识点回顾 规范的文件命名规范的文件夹管理机器学习项目的拆分编码格式和类型注解 昨天我们已经介绍了如何在不同的文件中&#xff0c;导入其他目录的文件&#xff0c;核心在于了解导入方式和python解释器检索目录的方式。 搞清楚了这些&#xff0c;那我们就可以来看看&#x…...

2025年电工杯新规发布-近三年题目以及命题趋势

电工杯将于2025.5.23 周五早八正式开赛&#xff0c;该竞赛作为上半年度竞赛规模最大的竞赛&#xff0c;因免报名费、一级学会承办等因素&#xff0c;被众多高校认可。本文将在从2025年竞赛新规、历史赛题选题分析、近年优秀论文分享、竞赛模板分析等进行电工杯备赛&#xff0c;…...

四元数中 w xyz 的含义及应用

四元数是一种用于表示三维空间中旋转的数学工具&#xff0c;形式通常为 qwxiyjzk&#xff0c;其中w 是实部&#xff0c;x,y,z 是虚部。它们的含义如下&#xff1a; 1. w&#xff08;实部&#xff09; 2. x,y,z&#xff08;虚部/向量部分&#xff09; 3. 单位四元数的条件 四元…...

CSS 样式表的四种应用方式详解以及css注释的应用

一、外部 CSS&#xff08;推荐方式&#xff09; 定义&#xff1a;将 CSS 代码保存为独立的 .css 文件&#xff0c;通过 <link> 标签引入 HTML。 优点&#xff1a; 实现内容与样式完全分离多个页面可共享同一 CSS 文件浏览器可缓存 CSS 文件&#xff0c;提升加载速度 …...

IntentUri页面跳转

android browser支持支持Intent Scheme URL语法的可以在wrap页面加载或点击时&#xff0c;通过特定的intent uri链接可以打开对应app页面&#xff0c;例如 <a href"intent://whatsapp/#Intent;schememyapp;packagecom.xiaoyu.myapp;S.help_urlhttp://Fzxing.org;end&qu…...

蓝桥杯2114 李白打酒加强版

问题描述 话说大诗人李白, 一生好饮。幸好他从不开车。 一天, 他提着酒显, 从家里出来, 酒显中有酒 2 斗。他边走边唱: 无事街上走&#xff0c;提显去打酒。 逢店加一倍, 遇花喝一斗。 这一路上, 他一共遇到店 N 次, 遇到花 M 次。已知最后一次遇到的是花, 他正好把酒喝光了。…...

[ 计算机网络 ] 深入理解OSI七层模型

&#x1f389;欢迎大家观看AUGENSTERN_dc的文章(o゜▽゜)o☆✨✨ &#x1f389;感谢各位读者在百忙之中抽出时间来垂阅我的文章&#xff0c;我会尽我所能向的大家分享我的知识和经验&#x1f4d6; &#x1f389;希望我们在一篇篇的文章中能够共同进步&#xff01;&#xff01;&…...

实战:基于Pangolin Scrape API,如何高效稳定采集亚马逊BSR数据并破解反爬虫?

引言&#xff1a;BSR数据——亚马逊运营的"指南针" 在竞争激烈的亚马逊市场&#xff0c;BSR (Best Sellers Rank) 数据已经成为卖家们不可或缺的"指南针"。这一数字化指标不仅反映商品在特定品类中的销售表现&#xff0c;更直接影响平台的流量分配和消费者…...

电子制造企业智能制造升级:MES系统应用深度解析

在全球电子信息产业深度变革的2025年&#xff0c;我国电子信息制造业正经历着增长与转型的双重考验。据权威数据显示&#xff0c;2025年一季度行业增加值同比增长11.5%&#xff0c;但智能手机等消费电子产量同比下降1.1%&#xff0c;市场竞争白热化趋势显著。叠加关税政策调整、…...

lambda架构和kappa架构区别

Lambda架构与Kappa架构是大数据处理领域的两种核心架构模式&#xff0c;主要差异体现在数据处理逻辑、系统复杂度和适用场景等方面。以下是二者的详细对比分析&#xff1a; 一、核心设计差异 ‌Lambda架构 包含三层&#xff1a;批处理层&#xff08;Batch Layer&#xff09;、…...

【css知识】flex-grow: 1

目录 一、基本概念&#xff1a;二、工作原理&#xff1a;多个元素的情况&#xff1a; 三、实际应用示例&#xff1a;常见使用场景&#xff1a;注意事项&#xff1a; 四、最佳实践&#xff1a;五、与其他 flex 属性配合&#xff1a; &#x1f680;写在最后 flex-grow: 1是什么&a…...

六足连杆爬行机器人的simulink建模与仿真

目录 1.课题概述 2.系统仿真结果 3.核心程序 4.系统原理简介 5.完整工程文件 1.课题概述 六足连杆爬行机器人的simulink建模与仿真。通过simulink&#xff0c;对六足机器人的六足以及机身进行simulink建模&#xff0c;模拟其行走&#xff0c;仿真输出机器人行走时六足的坐…...

软考软件测评师——系统安全设计(防火墙技术)

目录 一、核心概念解析 二、技术联动体系 三、技术局限认知 四、网络架构设计 五、防护系统测试规范 一、核心概念解析 1. 防火墙技术 网络安全基础防护体系&#xff0c;通过软硬件结合方式在内外部网络间建立隔离屏障。核心作用包括流量监控、访问控制、日志记录三大模块…...

漏洞检测与渗透检验在功能及范围上究竟有何显著差异?

漏洞检测与渗透检验是确保系统安全的重要途径&#xff0c;这两种方法各具特色和功效&#xff0c;它们在功能上有着显著的差异。 目的不同 漏洞扫描的主要任务是揭示系统内已知的安全漏洞和隐患&#xff0c;这就像是对系统进行一次全面的健康检查&#xff0c;看是否有已知的疾…...

探秘「4+3原型驱动的交付模式」如何实现软件快速定制

软件行业长期受困于需求沟通难题&#xff1a;客户需求表达不清、频繁变更及真伪需求混杂难辨&#xff1b;终端业务部门参与度低加剧后期确认困难&#xff0c;加之调研结果传递失真引发功能实现偏差——"需求黑洞"始终是甲乙双方的共同痛点。 然而&#xff0c;NC科技…...

C语言入门

一、认识C语言 什么是C语言 C语言是一门通用计算机编程语言&#xff0c;广泛应用于底层开发。C语言的设计目标是提供一种能以简易的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。 尽管C语言提供了许多低级处理的功能&#xff0c…...

java 异常验证框架validation,全局异常处理,请求验证

1、依赖 异常验证框架validation<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId><version>${spring-boot.version}</version></dependency> 当鼠标放在依…...

如何使用VH6501进行CAN采样点测试

Vector 的 VH6501 是一种专为 CAN 和 CAN FD 网络设计的干扰测试设备&#xff0c;集成了干扰生成和 CANoe 网络接口功能&#xff0c;支持通过 CAPL 脚本实现测试自动化。 硬件规格如下&#xff1a; VH6501采样点测试原理是&#xff1a;干扰一帧报文中某一位的采样点附近的总线电…...

QT6 源(113)篇二:阅读与注释工具栏 QToolBar,给出源码

&#xff08;9&#xff09;本源码来自于头文件 qtoolbar . h &#xff1a; #ifndef QDYNAMICTOOLBAR_H #define QDYNAMICTOOLBAR_H#include <QtWidgets/qtwidgetsglobal.h> #include <QtGui/qaction.h> #include <QtWidgets/qwidget.h>QT_REQUIRE_CONFIG(to…...

编程日志5.13

邻接表的基础代码 #include<iostream> using namespace std; //邻接表的类声明 class Graph {private: //结构体EdgeNode表示图中的边结点,包含顶点vertex、权重weight和指向下一个边结点的指针next struct EdgeNode { int vertex; int weight; …...

Java 08集合

集合 Collection 接口&#xff0c;不可以创建对象add clear remove contains(Object obj);判断是否存在 isEmpty 空返回为true sizeArrayList Collection<String> cnew ArraryList<>(); 以多态的方法创建集合对象&#xff0c;调用单列集合中的共有方法 编译看…...

CSS 背景全解析:从基础属性到视觉魔法

CSS 背景属性用于定义HTML元素的背景。 CSS 属性定义背景效果: background-color background-image background-repeat background-attachment background-position 一、background-color&#xff1a;背景颜色 作用&#xff1a;设置元素的背景色&#xff0c;支持所有合法…...

2025华为OD机试真题+全流程解析+备考攻略+经验分享+Java/python/JavaScript/C++/C/GO六种语言最佳实现

华为OD全流程解析&#xff0c;备考攻略 快捷目录 华为OD全流程解析&#xff0c;备考攻略一、什么是华为OD&#xff1f;二、什么是华为OD机试&#xff1f;三、华为OD面试流程四、华为OD薪资待遇及职级体系五、ABCDE卷类型及特点六、题型与考点七、机试备考策略八、薪资与转正九、…...

中小型制造业信息化战略规划指南

1 引言 在当今技术飞速发展和全球竞争日趋激烈的时代&#xff0c;信息化建设对于中小型制造企业&#xff08;SME&#xff09;而言&#xff0c;已不再是可有可无的选项&#xff0c;而是关乎生存、发展和保持持续竞争力的核心要素。在数字化浪潮席卷全球的背景下&#xff0c;制造…...

PowerBI 矩阵实现动态行内容(如前后销售数据)统计数据,以及过滤同时为0的数据

我们有一张活动表 和 一张销售表 我们想实现如下的效果&#xff0c;当选择某个活动时&#xff0c;显示活动前后3天的销售对比图&#xff0c;如下&#xff1a; 实现方法&#xff1a; 1.新建一个表&#xff0c;用于显示列&#xff1a; 2.新建一个度量值&#xff0c;用SELECTEDVA…...

在QT中栅格布局里套非栅格布局的布局会出现父布局缩放子布局不跟随的问题

这个是 Qt Designer 设计界面中的一个“常见陷阱”。 &#x1f9e0; 结论先说&#xff1a; 在 Qt Designer 中使用栅格布局&#xff08;Grid Layout&#xff09;嵌套其他栅格布局&#xff0c;一般不会出问题&#xff0c;但如果嵌套的是水平布局&#xff08;HBox&#xff09;或垂…...

Pydantic数据验证实战指南:让Python应用更健壮与智能

导读&#xff1a;在日益复杂的数据驱动开发环境中&#xff0c;如何高效、安全地处理和验证数据成为每位Python开发者面临的关键挑战。本文全面解析了Pydantic这一革命性数据验证库&#xff0c;展示了它如何通过声明式API和类型提示系统&#xff0c;彻底改变Python数据处理模式。…...

深度解析 HDFS与Hive的关系

1. HDFS 和 Hive 如何协同 我们将从 HDFS&#xff08;Hadoop Distributed File System&#xff09; 的架构入手&#xff0c;深入剖析其核心组成、工作机制、内部流程与高可用机制。然后详细阐述 Hive 与 HDFS 的关系&#xff0c;从执行流程、元数据管理、文件读写、计算耦合等…...

ArrayList源码分析

1. ArrayList默认初始化容量 首先编写一个简单的初始化ArrayList的代码 List<String> li new ArrayList<>();然后进入ArrayList中&#xff0c;在无参数构造方法中可以查看到上面的绿色注释中写了构造一个空的集合并且初始化容量为10。接下来继续查看源码&#x…...

文件操作和IO-2 使用Java操作文件

Java操作文件的API 1、针对文件系统的操作。包括但不限于&#xff1a;创建文件、删除文件、重命名文件、列出目录内容…… 2、针对文件内容的操作。读文件/写文件 Java中针对文件的操作&#xff0c;使用File类来进行操作&#xff0c;这个类被存储在java.io这个包里面。 i&a…...

day 31

文件的拆分 1. 项目核心代码组织 src/&#xff08;source的缩写&#xff09;&#xff1a;存放项目的核心源代码。 2. 配置文件管理 config/ 目录&#xff1a;集中存放项目的配置文件&#xff0c;方便管理和切换不同环境&#xff08;开发、测试、生产&#xff09;的配置。 …...

基于Python批量删除文件和批量增加文件

一、为什么写这么一个程序 其实原因也是很简单的&#xff0c;我去网上下载了一个文件夹&#xff0c;里面太多别人的文件了&#xff0c;我不喜欢&#xff0c;所以我就写了这么一个代码。 二、安装Python和vscode 先安装Python在安装vscode Python安装 vscode的安装 三、源码…...

【信息系统项目管理师】第12章:项目质量管理 - 26个经典题目及详解

更多内容请见: 备考信息系统项目管理师-专栏介绍和目录 文章目录 【第1题】【第2题】【第3题】【第4题】【第5题】【第6题】【第7题】【第8题】【第9题】【第10题】【第11题】【第12题】【第13题】【第14题】【第15题】【第16题】【第17题】【第18题】【第19题】【第20题】【第…...