【Linux】线程:生产者-消费者模型 & 信号量

线程知识合集:
【Linux】线程概念 && 线程控制(接口详谈)&& 僵尸线程的产生和解决
【Linux】线程安全:互斥 & 同步
【Linux】线程安全:死锁(附加gdb调试分析)



生产者-消费者模型

场景引入

对于吃面-做面模型来说,其临界资源就是一个全局变量g_bowl = 0 / 1,也就是该临界资源只有一个;


如果设计一个类似于微信后端的程序:一个用户发送给另一个用户的消息,实际上是先发送至微信后端服务器处理后,再由服务器转发给另一个用户。我们可以将后端大致分为三种线程:接受消息线程,消息处理线程,发送数据线程;

在该场景中,服务器储存用户消息的内存就是临界资源。如果临界资源只有一个,那么为了保证资源安全,一个用户将消息发送给微信服务器,在服务器处理完消息并发送前,服务器的不会接收其他用户发送的消息,这种情况实际上肯定要避免。
在这里插入图片描述

为了实现高并发(同时可处理多个用户消息),就要让服务器的临界资源远不止一个,一般就是设立一个队列,队列的大小就是临界资源的个数,队列的每个元素都存储(或指向)了一条用户的消息,且该消息队列必须保证线程安全;

但是一般接受消息、发送数据线程的执行速度远快于消息处理线程,如果一条数据没有被处理完毕,该线程内容需一直在临界资源,而速度更快的接受消息线程因为临界资源空间不足,就会陷入等待。

为了处理消息的速度更快,我们需实现各类型线程解耦(接受消息、处理消息、发送消息互不干扰)和忙闲不均(资源队列作相当于缓冲区,避免双方线程一个阻塞,导致另一个阻塞),设置多个临界资源队列即可。
在这里插入图片描述


生产者-消费者模型的结构

通过一个简单的生产者-消费者模型,了解多临界资源的机制:

就是通过一个容器(这里使用队列)存放临界资源,使生产者和消费者解耦

解耦:生产者线程和消费者线程不直接通讯。
生产线程生成的数据不用等待消费线程处理后再继续,而是直接放入临界资源队列中;
消费线程读取线程不通过生产线程,而是直接读取临界资源队列即可;

临界资源队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力;
临界资源队列必须保证线程安全:

生产线程之间不能同时向资源队列写入;
消费线程之间不能同时向资源队列读取;
生产线程和消费线程之间也不能同时操作资源队列;


多临界资源机制的优点

在这里插入图片描述


生产者-消费者模型的实现

123规则:

  • 一个线程安全的队列;
    在这里插入图片描述

  • 两种角色的线程:生产者线程、消费者线程;

  • 三个规则;
    在这里插入图片描述

源码:prod_cons.cpp

#include <stdio.h>
#include <unistd.h>
#include <queue>
#include <pthread.h>
#define THREAD_COUNT 2
using namespace std;

// 1.一个线程安全的队列
// 封装一个安全队列的类
class safe_queue 
{
public:
    //构造
    safe_queue(size_t capacity = 1)
        :g_data(0) 
        ,_capacity(capacity)
    {
        //初始化互斥锁和条件变量
        pthread_mutex_init(&_q_lock, NULL);
        pthread_cond_init(&_prod_cond, NULL);
        pthread_cond_init(&_cons_cond, NULL);
    }
    //析构
    ~safe_queue()
    {
        //销毁互斥锁和条件变量
        pthread_mutex_destroy(&_q_lock);
        pthread_cond_destroy(&_prod_cond);
        pthread_cond_destroy(&_cons_cond);
    }
    //生产线程的push接口
    void Push()
    {
        pthread_mutex_lock(&_q_lock);
        while(_q.size() >= _capacity)  //当资源队列已满,生产线程等待
        {
            pthread_cond_wait(&_prod_cond, &_q_lock);
        }
        _q.push(++g_data);
        printf("Product_thread: %p, make %d\n", pthread_self(), g_data);
        pthread_cond_signal(&_cons_cond);
        pthread_mutex_unlock(&_q_lock);
    }
    //消费线程的pop接口
    void Pop()
    {
        pthread_mutex_lock(&_q_lock);
        while(_q.size() <= 0)  //当资源队列为空,消费线程等待
        {
            pthread_cond_wait(&_cons_cond, &_q_lock);
        }
        printf("Consume_thread: %p, get %d\n", pthread_self(), _q.front());
        _q.pop();
        pthread_cond_signal(&_prod_cond);
        pthread_mutex_unlock(&_q_lock);
    }

private:
    queue<int> _q;  //队列(临界资源)
    int g_data;  //队列资源数序号
    size_t _capacity;  //预设队列大小
    pthread_mutex_t _q_lock;  //队列的互斥锁
    pthread_cond_t _prod_cond;  //生产线程的条件变量
    pthread_cond_t _cons_cond;  //消费线程的条件变量
};

// 生产线程入口函数
void* prod_start(void* arg)
{
    safe_queue* Que = (safe_queue*)arg;
    while(1)
    {
        Que->Push();
    }
}

// 消费线程入口函数
void* cons_start(void* arg)
{
    safe_queue* Que = (safe_queue*)arg;
    while(1)
    {
        Que->Pop();
    }
}

int main()
{
    // 初始化一个资源队列(线程安全)
    size_t n = 1;
    safe_queue* Que = new safe_queue(n);

    // 创建线程
    int i = 0;
    pthread_t prod_t[THREAD_COUNT];
    pthread_t cons_t[THREAD_COUNT];
    for(; i < THREAD_COUNT; ++i)
    {
        int ret = pthread_create(&prod_t[i], NULL, prod_start, (void*)Que);
        if(ret < 0)
        {
            perror("pthread_create_prod");
            return 0;
        }

        ret = pthread_create(&cons_t[i], NULL, cons_start, (void*)Que);
        if(ret < 0)
        {
            perror("pthread_create_prod");
            return 0;
        }
    }

    // 线程等待
    for(i = 0; i < THREAD_COUNT; ++i)
    {
        pthread_join(prod_t[i], NULL);
        pthread_join(cons_t[i], NULL);
    }

    // 销毁队列
    delete Que;
    return 0;
}

信号量

信号量是什么

若想保证线程同步,之前提到可以采用条件变量来控制线程进入PCB等待序列,但使用条件变量麻烦之处在于,需要程序员自己判断条件是否满足。
在这里插入图片描述

而信号量,相当于封装了上述的第2步和第4步,使用信号量,程序员无需判断线程是否满足条件;

  • 条件变量的本质是一个PCB等待队列,而信号量的本质就是一个资源计数器+PCB等待队列;
  • 使用信号量,需在初始化时添加该线程的可使用资源数;
  • 每个信号量都有P(等待接口)、V(释放接口)两种操作;

    P操作相当于上述步骤2,该操作会先将计数器资源-1(非原子操作)

    -1后,若资源计数器值<0,说明此时无可用资源,会将该线程放入PCB等待序列阻塞等待;
    若资源计数器值>=0,说明有资源可用,直接执行后续代码即可;


    V操作相当于上述步骤4,该操作会先将计数器资源+1

    +1后,若资源计数器值<=0,说明该信号量对应的PCB等待队列有线程等待,那么唤醒PCB等待队列的线程;
    若资源计数器值>0,说明没有线程在等待,不用执行任何操作;


信号量的接口

在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


信号量的注意事项

  • 信号量可用于线程或进程;

    信号量必须实现不同线程(或进程)访问到同一个,否则无意义,因此:
    在线程中使用,信号量必须开辟在当前进程的全局数据区(即设为全局变量),以便所有线程访问;
    在进程中使用,信号量必须开辟在共享内存中(进程通信);

  • 等待接口sem_wait是P操作,释放接口sem_post是V操作;

  • sem_wait等待接口不会对互斥锁做任何操作,因此在使用时,必须若先加锁,再执行信号量P操作;
    在这里插入图片描述

  • 信号量不止可以实现同步功能,也可以实现互斥功能;

    互斥锁本质就是一个临界资源只能同时被一个线程所访问,那么让信号量的资源量变为1,所有线程要访问该临界资源时,必须先进行信号量P操作,若资源占用,则进入PCB等待队列,这个流程和互斥锁一模一样;


信号量实现生产者-消费者模型

  • 通过环形队列存放生产者生成的资源数据;
  • 初始,生产者可使用资源数:环形队列的大小;消费者可使用资源数:0;

源码:sem.cpp

// 通过信号量实现“生产者-消费者模型”
#include <stdio.h>
#include <unistd.h>
#include <vector>
#include <pthread.h>
#include <semaphore.h>

#define CAPACITY 1  //资源队列大小 
#define THREADCOUNT 2  //每种线程数量

using namespace std;

/* 1.定义一个线程安全的队列
 *     队列:环形队列(数组模拟)
 *     线程安全:
 *         同步:信号量实现
 *         互斥:信号量实现
 * */
class SafeQueue
{
    public:
        // 构造
        SafeQueue()
            : _capacity(CAPACITY)
            , _vec(CAPACITY)
        {
            // 初始化信号量
            sem_init(&sem_mutex, 0, 1);  //互斥信号量(初始资源数为1)
            sem_init(&sem_cons_cond, 0, 0);  //消费者信号量(初始资源数为0)
            sem_init(&sem_prod_cond, 0, CAPACITY);  //生产者信号量(初始资源为队列长度)

            _pos_write = 0;
            _pos_read = 0;
        }
        // 析构
        ~SafeQueue()
        {
            // 销毁信号量
            sem_destroy(&sem_mutex);
            sem_destroy(&sem_cons_cond);
            sem_destroy(&sem_prod_cond);
        }
        // 消费者线程的POP接口
        void Pop()
        {
            sem_wait(&sem_cons_cond);
            sem_wait(&sem_mutex);

            int cur = _vec[_pos_read];
            _pos_read = (_pos_read + 1)%_capacity;  //更新_pos_read
            printf("Consume:%p get:%d\n", pthread_self(), cur);

            sem_post(&sem_prod_cond);
            sem_post(&sem_mutex);
        }
        // 生产者线程的PUSH接口
        void Push(int data)
        {
            sem_wait(&sem_prod_cond);
            sem_wait(&sem_mutex);

            printf("Product:%p make:%d\n", pthread_self(), data);
            _vec[_pos_write] = data;
            _pos_write = (_pos_write + 1)%_capacity;

            sem_post(&sem_cons_cond);
            sem_post(&sem_mutex);
            
        }

    private:
        size_t _capacity;  //数组容量
        vector<int> _vec;  //环形队列数组
        
        sem_t sem_mutex;  //队列资源互斥信号量
        sem_t sem_cons_cond;  //消费者线程的同步信号量
        sem_t sem_prod_cond;  //生产者线程的同步信号量

        int _pos_write;  //队列末尾,插入元素位置
        int _pos_read;  //队列头部,读取元素位置
};

// 消费者线程入口函数
void* cons_strat(void* arg)
{
    SafeQueue* sq = (SafeQueue*)arg;
    while(1)
    {
        sq->Pop();
        sleep(2);
    }
}

//生产者向循环队列存放的数据
int g_data = 0;  
//设置静态互斥锁,不同生产者对g_data必须互斥访问
pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;

// 生产者线程入口函数
void* prod_strat(void* arg)
{
    SafeQueue* sq = (SafeQueue*)arg;
    while(1)
    {
        pthread_mutex_lock(&g_lock);
        sq->Push(g_data++);
        pthread_mutex_unlock(&g_lock);
        sleep(1);
    }
}

// 创建生产者-消费者两种线程
int main(){
    SafeQueue* sq = new SafeQueue();

    pthread_t cons[THREADCOUNT], prod[THREADCOUNT];
    for(int i = 0; i < THREADCOUNT; i++){
        int ret = pthread_create(&cons[i], NULL, cons_strat, (void*)sq);
        if(ret < 0)
        {
            perror("pthread_create");
            return 0;
        }      
        ret = pthread_create(&prod[i], NULL, prod_strat, (void*)sq);
        if(ret < 0)
        {
            perror("pthread_create");
            return 0;
        }
    }
    for(int i = 0; i < THREADCOUNT; i++)
    {
        pthread_join(cons[i], NULL);
        pthread_join(prod[i], NULL);
    }
    delete sq;
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux信号量可以用来实现生产者-消费者模型,保证生产者消费者之间的同步和互斥。下面是一个基于信号量生产者-消费者模型的示例代码: ```c #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <semaphore.h> #define BUFFER_SIZE 10 int buffer[BUFFER_SIZE]; sem_t empty, full; pthread_mutex_t mutex; void *producer(void *arg) { int item = 0; while (1) { // 等待空闲缓冲区 sem_wait(&empty); // 获取互斥锁 pthread_mutex_lock(&mutex); // 生产数据项 item++; // 将数据项放入缓冲区 buffer[(item - 1) % BUFFER_SIZE] = item; printf("Produced item %d\n", item); // 释放互斥锁 pthread_mutex_unlock(&mutex); // 发送信号通知缓冲区中已有数据 sem_post(&full); } } void *consumer(void *arg) { int item; while (1) { // 等待缓冲区中有数据 sem_wait(&full); // 获取互斥锁 pthread_mutex_lock(&mutex); // 从缓冲区取出数据项 item = buffer[item % BUFFER_SIZE]; printf("Consumed item %d\n", item); // 释放互斥锁 pthread_mutex_unlock(&mutex); // 发送信号通知缓冲区有空闲 sem_post(&empty); } } int main(int argc, char *argv[]) { pthread_t ptid, ctid; // 初始化信号量和互斥锁 sem_init(&empty, 0, BUFFER_SIZE); sem_init(&full, 0, 0); pthread_mutex_init(&mutex, NULL); // 创建生产者消费者线程 pthread_create(&ptid, NULL, producer, NULL); pthread_create(&ctid, NULL, consumer, NULL); // 等待线程结束 pthread_join(ptid, NULL); pthread_join(ctid, NULL); return 0; } ``` 在这个示例中,用两个信号量`empty`和`full`分别表示缓冲区的空闲和有数据状态。生产者线程在生产数据项之前,会等待`empty`信号量,表示缓冲区中有空闲的位置。生产完成后,会释放一个`full`信号量,表示缓冲区中有数据了。消费者线程在消费数据项之前,会等待`full`信号量,表示缓冲区中有数据。消费完成后,会释放一个`empty`信号量,表示缓冲区中有空闲位置了。在生产者消费者之间,用一个互斥锁`mutex`来保护共享缓冲区的数据。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值