生产者消费者模型用于解决生产者和消费者之间的同步问题。通过创建多个线程作为生产者和消费者,生产者消费者模型利用线程间的同步机制(如互斥锁、条件变量、信号量等)来实现生产者和消费者之间的协作和数据共享。本篇采用信号量+BlockQueue来实现生产者消费者模型。本文用到的一些接口(线程创建、线程等待...)请点击这个链接查看:
一、生产者消费者模型的理解
1.生产者消费者模型的了解
2.生产者消费者模型的3个重要规则
一、3种关系
生产者之间是: 竞争—互斥
消费者之间是:竞争—互斥
生产者和消费者之间是:互斥和同步
二、2种角色
生产者、消费者。
三、1个交易场所
共同操作的临界资源。
二、对临界资源的操作(加锁、解锁...)
1.生产者消费者模型为什么要引入加解锁操作?
因为当我们实现多个生产者消费者模型时,生产者之间是: 竞争—互斥,消费者之间是:竞争—互斥,所以当多个生产者或者消费者都想访问同一个临界资源时要进行加解锁。
2.接口函数的使用方法
1.以下接口函数头文件均是:#include<pthread.h>
2.使用时一般通过全局变量创建锁 pthread_mutex_t mutex;
1.pthread_mutex_init( )
1.函数原型:
int pthread_mutex_init(pthread_mutex_t * mutex, const pthread_mutexattr_t * attr);
2.函数参数:
mutex
:指向要初始化的互斥锁的指针。attr
:指向互斥锁属性的指针,通常可以传入nullptr
表示使用默认属性。3.函数的用法:
用于初始化一个互斥锁(mutex),并可以设置互斥锁的属性。
pthread_mutex_init(&mutex , nullptr);
4.函数返回值:
若成功返回0,否则返回错误编号
2.pthread_mutex_destroy( )
1.函数原型:
int pthread_mutex_destroy(pthread_mutex_t * mutex);
2.函数参数:
mutex
:指向要销毁的互斥锁的指针。3.函数的用法:
用于销毁一个互斥锁(mutex),在销毁之前应确保该互斥锁已经被初始化过。 pthread_mutex_destroy(&mutex);
4.函数返回值:
若成功返回0,否则返回错误编号
3.pthread_mutex_lock( )和pthread_mutex_unlock( );
1.作用:pthread_mutex_lock() 用于锁定互斥量(mutex),防止其他线程访问共享资源,而 pthread_mutex_unlock() 则用于解锁互斥量,允许其他线程访问共享资源。如果第二个线程在调用pthread_mutex_lock()时没有成功获取到锁,它会被阻塞等待直到锁可用为止。
4.四个接口函数的综合使用
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;
pthread_mutex_t mutex; // 全局变量
void *Routine(void *args)
{
string name = static_cast<const char *>(args);
int cnt = 1000;
// pthread_mutex_lock(&mutex); //加锁
while (cnt)
{
cout << "I am " << name << " cnt:" << cnt-- << endl;
}
// pthread_mutex_unlock(&mutex); //解锁
return nullptr;
}
int main()
{
// 创建两个线程进行打印操作
pthread_mutex_init(&mutex, nullptr); // 局部锁用pthread_mutex_init()函数
pthread_t tid1, tid2;
pthread_create(&tid1, nullptr, Routine, (void *)"Thread-1");
pthread_create(&tid2, nullptr, Routine, (void *)"Thread-2");
pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
pthread_mutex_destroy(&mutex);
return 0;
}
现象说明,由图可见。一个线程并没有完全执行完循环代码。原因是由于两个线程共享标准输出流(显示器),因此它们在打印时会互斥地竞争这个临界资源,即显示器。当一个线程的时间片用完但还没有执行完代码时,操作系统会调度下一个线程来执行,这样就会导致两个线程交替打印输出。
当我们把加锁解锁的代码注释删除后,再编译执行就不会再出现交替打印的情况了。原因是:如果线程1在持有锁的情况下时间片用完而没有执行完,它仍然会持有锁,其他线程无法申请到锁。会阻止其他线程继续执行临界区内的代码,所以保证了按照正常打印(实现了有序的访问临界资源—同步)。
三、信号量
信号量(类型sem_t)被广泛的应用于线程和进程之间的同步与互斥,信号量的本质其实是一个非负的整数计数器,它被用来控制对公共资源的访问,当信号量大于0的时候,才允许访问不会发生阻塞。
以下接口函数均需要使用的头文件:#include <semaphore.h>
1.信号量接口函数
1.sem_init( )
int sem_init(sem_t *sem, int pshared, unsigned int value);
1.功能:
用于初始化一个信号量,参数分别代表信号量的指针、pshared和value。
2.参数分别表示:
- 第一个参数是指向要初始化的信号量的指针。
- 第二个参数pshared指定信号量的类型,如果为0,则信号量只能在同一进程的线程之间共享;如果为非零值,则信号量可以在不同进程之间共享。
- 第三个参数value指定信号量的初始值(有多少个资源可用)。
3.返回值:
如果函数执行成功,则返回0;如果出现错误,则返回-1。
2.sem_wait( )(P操作)
1.函数原型:
int sem_wait(sem_t * sem);
2.函数参数:
sem表示进行操作的信号量。
3.函数的作用:
sem_wait(&sem)对要操作的信号量进行-1,如果信号量=0,则对应线程要阻塞等待,直到其他线程释放信号量(调用sem_post(&sem)增加信号量的值)使得信号量的值变为大于0时,阻塞的线程才能继续执行。
4.函数的返回值:
如果函数执行成功,则返回0;如果出现错误,则返回-1。
3.sem_post( )(V操作)
1.函数的原型:
int sem_post(sem_t * sem);
2.函数的参数:
sem表示进行操作的信号量。
3.函数的作用:
sem_wait(&sem)对要操作的信号量进行+1("释放信号量")。
4.函数的返回值:
如果函数执行成功,则返回0;如果出现错误,则返回-1。
4.sem_destroy( )
1.函数的原型:
int sem_destroy(sem_t * sem);
2.函数的参数:
sem表示进行操作的信号量(销毁)。
3.函数的作用:
销毁一个信号量,并释放相关的资源。
4.函数的返回值:
如果函数执行成功,则返回0;如果出现错误,则返回-1。
四、生产者消费者模型的实现
1.理论理解部分:
2.代码实现部分:
#pragma once
#include <iostream>
#include <vector>
#include <semaphore.h> //信号量头文件
using namespace std;
const int defaultsize = 5;
template <class T>
class RingQueue
{
private:
// 封装P,V操作
void P(sem_t &sem)
{
sem_wait(&sem); // 用于等待信号量的值大于0,然后将信号量的值减1。如果信号量的值为0,则调用线程将被阻塞,直到信号量的值变为大于0。
}
void V(sem_t &sem)
{
sem_post(&sem); // 用于增加信号量的值。当有线程使用sem_wait函数等待信号量时,其他线程可以使用sem_post函数增加信号量的值,以唤醒等待的线程。
}
public:
RingQueue(int size = defaultsize)
: _ringqueue(size), _size(size), _p_step(0), _c_step(0)
{
// 对信号量进行初始化
// 第一个参数是指向信号量对象的指针,
// 第二个参数是用于指定信号量的共享性质,通常为0表示信号 量只能在同一进程内共享,
// 第三个参数是信号量的初始值,表示信号量的初始可用资源数量。
sem_init(&_space_sem, 0, size);
sem_init(&_data_sem, 0, 0);
}
~RingQueue()
{
// 对信号量进行销毁
sem_destroy(&_space_sem);
sem_destroy(&_data_sem);
}
// push,生产操作
void Push(const T &in)
{
P(_space_sem); // 申请到了空间信号量,可以生产数据了。
_ringqueue[_p_step] = in; //数组赋值
_p_step++;
_p_step %= _size;
V(_data_sem); // 释放数据信号量,唤醒消费者
}
// pop,消费操作
void Pop(T *out) // 用指针接收
{
P(_data_sem);
*out = _ringqueue[_c_step]; // 给data变量赋值
_c_step++;
_c_step %= _size;
V(_space_sem); // 释放空间信号量 唤醒生产者
}
private:
vector<T> _ringqueue;
int _size;
int _p_step; // 生产者的位置
int _c_step; // 消费者的位置
sem_t _space_sem; // 生产者信号量
sem_t _data_sem; // 消费者信号量
};
#include "RingQueue.hpp"
#include <pthread.h>
#include <unistd.h>
void *Productor(void *args)
{
RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
// 执行生产任务
int cnt = 100;
while (1)
{
rq->Push(cnt);
cout << "生产者生产了数据:" << cnt << endl;
cnt--;
//sleep(1);
}
}
void *Consumer(void *args)
{
RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
// 执行消费任务
while (1)
{
int data = 0;
rq->Pop(&data); //传入一个变量给Pop函数
cout << "消费者得到了数据:" << data << endl;
}
}
int main()
{
pthread_t c, p;
RingQueue<int> *rq = new RingQueue<int>(); // 使用 默认5大小去初始化
pthread_create(&p, nullptr, Productor, rq); // 通过传递rq使得生产者消费者可以操作同一块资源(RingQueue)
pthread_create(&c, nullptr, Consumer, rq);
pthread_join(p, nullptr);
pthread_join(c, nullptr);
return 0;
}
可以用添加sleep()函数控制生产者消费者生产消费的速度,进一步理解。
总结
本文简单介绍了生产者消费者模型和一些前提接口函数。本文是一个简单的单生产单消费的模型,后续本专栏会继续更新,多生产多消费的模型(其实只需进行加解锁操作即可实现(因为生产者和生产者彼此之间是互斥的关系,消费者之间也是互斥的))。同时后续会继续在本专栏更新多线程部分的文章,希望对大家有所帮助。