【Linux】线程同步和生产消费模型

目录

条件变量

接口

简单使用

生产消费模型

生产消费模型代码


条件变量

上篇博客我们介绍了线程互斥 ,是通过加锁的方式把共享资源保护起来变成临界资源,同一时刻只允许一个线程访问临界资源

但是不同的线程申请锁的能力是有差别的,就拿抢票的代码举例,如果一个线程申请锁的能力很强,此时票全被一个线程抢走那也是不合理的,为了解决这种不合理的现象,我们就引入了线程同步 的概念,简单来说就是线程之间去排队,让所有的线程访问临界资源具有一定的顺序性,这样线程之间就不会有饥饿问题了

那么如何实现线程同步呢?我们是引入条件变量 ,我们可以简单的理解为条件变量就是维护着一个等待队列(wait)和一个通知机制(signal),这个通知机制就是为了唤醒等待队列中的线程

接口

条件变量跟互斥锁一样,就是一个某种类型创建的变量,我们来看看创建和使用条件变量的一些接口

man pthread_cond_init

这些接口和互斥锁的接口可以说是大同小异,同样还是分定义局部的还是全局的条件变量,它们通过不同方法定义

man pthread_cond_wait

这个就是说让执行此接口的线程去条件变量下等待,直到有人唤醒

man pthread_cond_signal

这个就是唤醒等待的线程,broadcast是广播的意思,就是唤醒所有在这个条件变量下等待的线程,signal就是只唤醒一个等待的线程

简单使用

上面我们已经说完了接口,下面我们就来简单用一下,写一个简单的测试代码,看看能不能实现线程间同步

我们打算创建一个主线程,创建一批新线程,新线程去条件变量下等待,然后主线程负责发信号给新线程

cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
#include <memory>
#include <unistd.h>
#include <pthread.h>

pthread_cond_t gcond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;
void *Mastercode(void *args)
{
    char *name = static_cast<char *>(args);
    while (1)
    {
        usleep(100000);
        pthread_cond_signal(&gcond);
        std::cout << name << " 唤醒了一个线程 " << std::endl;
    }
    delete[] name;
}
void *Slavercode(void *args)
{
    char *name = static_cast<char *>(args);
    while (1)
    {
        pthread_mutex_lock(&gmutex);
        pthread_cond_wait(&gcond, &gmutex);
        std::cout << name << " 被唤醒 " << std::endl;
        pthread_mutex_unlock(&gmutex);
    }
    delete[] name;
}
void StartMaster(std::vector<pthread_t> *tids)
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, Mastercode, (void *)"Master");
    if (n == 0)
    {
        std::cout << "create master success" << std::endl;
        tids->push_back(tid);
    }
}
void StartSlavers(std::vector<pthread_t> *tids, int num)
{
    for (int i = 1; i <= num; i++)
    {
        pthread_t tid;
        char *name = new char[64];
        snprintf(name, 64, "slaver-%d", i);
        int n = pthread_create(&tid, nullptr, Slavercode, (void *)name);
        if (n == 0)
        {
            std::cout << "create " << name << std::endl;
            tids->push_back(tid);
        }
    }
}
void WaitAll(std::vector<pthread_t> &tids)
{
    for (auto &e : tids)
    {
        pthread_join(e, nullptr);
    }
}
int main()
{
    std::vector<pthread_t> tids;
    StartMaster(&tids);
    StartSlavers(&tids, 5);
    WaitAll(tids);

    return 0;
}

我们可以看到结果就是主线程唤醒的新线程是有顺序的,就是因为新线程是在等待队列中进行排队等待的

生产消费模型

什么是生产消费模型呢?顾名思义,有生产者,有消费者,生产者生产数据给消费者。

简单来说,就是一种数据传输,并且是一种并发传递数据,因为生产者在生产任务(数据)的时候,消费者可以从缓冲区中拿任务;而我们之前比如传参或进程间通信也是一种数据的传输,只不过这时串行的传输
我们简单举一个超市的例子,超市有它不同的供应商(生产者),还有不同的顾客(消费者)

超市内的资源就是共享资源,由于生产者们之间,消费者们之间,生产者和消费者之间不能同时访问超市这种资源,也就是说它们之间是互斥的(为了方便起见这么理解,实际情况复杂更多,并且实际的生产消费模型也就是较为简单的这种情况),所以我们需要对超市进行保护,让超市中的资源变成临界资源。并且供应商们和顾客们都是线程。

超市中的资源是临界资源,那超市是什么呢?超市其实就是保存数据的内存空间 ,并且为了保存数据更加方便,通常用一种特定的数据结构对象来作为数据交易(生产者把数据交给消费者)的场所。

生产者和消费者之间除了互斥还有同步关系,如果超市中的资源满了,那么生产者就不要生产了,同样,没有资源了,消费者就要等待。这体现的就是一种同步关系

所以,我们要记住生产消费模型,就要记住三种关系(生生,生消,消消),两种角色,一个交易场所即可
为什么使用生产消费模型呢?它有什么好处呢?

1.它可以提供比较好的并发度,供应商生产产品的时候,消费者可以去超市买东西;消费者使用产品时,供应商可以往超市中放东西。

2.生产和消费数据,可以进行解耦,实际就是生产任务由一个线程管,执行任务由一个线程管,它们之间关系很小

3.支持忙闲不均,这个主要是通过引入缓冲区(超市)来实现的,如果没有缓冲区,生产者一会忙,一会闲,消费者就要被迫的这样,有了缓冲区消费者就可以平稳的执行任务,其实就是平衡二者之间处理能力的差异,减小等待时间,提高整体的效率

生产消费模型代码

我们需要考虑用什么来充当临时存放数据的缓冲区,我们可以自定义一个阻塞队列 ,它的特点就是队列中放满了就等消费者消费,如果没有就等生产者生产 ,并且最好生产者生产了要通知消费者一下,如果消费者消费了最好通知生产者去生产。这个其实不就用到了条件变量中的等待和通知机制嘛,我们下面的代码就是先不用我们之前封装的pthread库,我们直接用系统调用:

cpp 复制代码
//BlockQueue.hpp
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>

template <class T>
class BlockQueue
{
    bool isfull()
    {
        return _q.size() == _cap;
    }
    bool isempty()
    {
        return _q.empty();
    }

public:
    BlockQueue(int cap = 6)
        : _cap(cap)
    {
        _producer_wait_num = 0;
        _consumer_wait_num = 0;
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond_consumer, nullptr);
        pthread_cond_init(&_cond_producer, nullptr);
    }

    void Enqueue(const T &data)
    {
        pthread_mutex_lock(&_mutex);
        while (isfull())
        {
            _producer_wait_num++;
            pthread_cond_wait(&_cond_producer, &_mutex);
            _producer_wait_num--;
        }
        _q.push(data);
        if (_consumer_wait_num > 0)
            pthread_cond_signal(&_cond_consumer);
        pthread_mutex_unlock(&_mutex);
    }
    void Pop(T *out)
    {
        pthread_mutex_lock(&_mutex);
        while (isempty())
        {
            _consumer_wait_num++;
            pthread_cond_wait(&_cond_consumer, &_mutex);
            _consumer_wait_num--;
        }
        *out = _q.front();
        _q.pop();
        if (_producer_wait_num > 0)
            pthread_cond_signal(&_cond_producer);
        pthread_mutex_unlock(&_mutex);
    }

    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond_consumer);
        pthread_cond_destroy(&_cond_producer);
    }

private:
    std::queue<T> _q;
    int _cap;
    pthread_mutex_t _mutex;
    pthread_cond_t _cond_producer;
    pthread_cond_t _cond_consumer;

    int _producer_wait_num;
    int _consumer_wait_num;
};


//Task.hpp
#pragma once
#include<iostream>
#include <string>
#include<functional>
// using Task=std::function<void()>;

// void printhello()
// {
//     std::cout<<"hello world"<<std::endl;
// }

class Task
{
public:
    Task() {}
    Task(int a, int b) : _a(a), _b(b)
    {
    }
    std::string result_to_string()
    {
        return std::to_string(_a) + " + " + std::to_string(_b) + " = " + std::to_string(_a+_b);
    }
    std::string question_to_string()
    {
        return std::to_string(_a) + " + " + std::to_string(_b) + " = ?";
    }

private:
    int _a;
    int _b;
};

//Main.cc
#include "BlockQueue.hpp"
#include "Task.hpp"
#include <iostream>
#include <vector>
#include <unistd.h>
#include <pthread.h>
#include <ctime>
using std::cout;
using std::endl;
using typeinbp = Task;
using typeofbq = BlockQueue<typeinbp>;

struct ThreadData
{
    ThreadData(const std::string &str = "no_name", typeofbq *pbq = nullptr)
        : _name(str), _pbq(pbq) {}

    std::string _name;
    typeofbq *_pbq;
};

void *producercode(void *args)
{
    ThreadData *ptd = static_cast<ThreadData *>(args);
    while (1)
    {
        int a = rand() % 100;
        usleep(1234);
        int b = rand() % 100;
        typeinbp tmp(a, b);
        // ptd->_pbq->Enqueue(printhello);
        ptd->_pbq->Enqueue(tmp);
        cout << ptd->_name << " product a ques " << tmp.question_to_string() << endl;
        sleep(1);
    }
    delete ptd;
    return nullptr;
}
void *consumercode(void *args)
{
    ThreadData *ptd = static_cast<ThreadData *>(args);
    while (1)
    {
        typeinbp tmp;
        ptd->_pbq->Pop(&tmp);
        // tmp();
        cout << ptd->_name << " get a message: " << tmp.result_to_string() << endl;
    }
    delete ptd;
    return nullptr;
}

void startproducer(std::vector<pthread_t> *ppids, int num, typeofbq *pbq)
{
    for (int i = 1; i <= num; i++)
    {
        std::string name = "producer_" + std::to_string(i);
        ThreadData *ptd = new ThreadData(name, pbq);
        pthread_t pid;
        pthread_create(&pid, nullptr, producercode, (void *)ptd);
        ppids->push_back(pid);
    }
}
void startconsumer(std::vector<pthread_t> *ppids, int num, typeofbq *pbq)
{
    for (int i = 1; i <= num; i++)
    {
        std::string name = "consumer_" + std::to_string(i);
        ThreadData *ptd = new ThreadData(name, pbq);
        pthread_t pid;
        pthread_create(&pid, nullptr, consumercode, (void *)ptd);
        ppids->push_back(pid);
    }
}
void waitall(std::vector<pthread_t> &pids)
{
    for (auto &pid : pids)
    {
        pthread_join(pid, nullptr);
    }
}
int main()
{
    srand(time(nullptr));
    std::vector<pthread_t> pids;
    typeofbq bq(5);
    startproducer(&pids, 3, &bq);
    startconsumer(&pids, 4, &bq);
    waitall(pids);

    return 0;
}

这段代码有几个关键点需要解释:

signal放unlock之前之后都可以,signal放之前就是先通知再解锁,一解锁别的线程就可以得到锁了;signal放之后就是先解锁再通知,这样也是可以的

这里要用while而不能用if,这其实就跟条件变量的特点有关,一个线程pthread_cond_wait之后就会释放掉锁,因为它不可能持有锁去等待,那样就没人唤醒它了。等到wait被唤醒之后此线程需要重新去竞争锁,万一被别人争到了,等此线程争到了锁后条件又不满足了,此时用if的话,它还是向下走,此时就会出错,所以要用while要重新判断条件

设置这两个变量就是不要生产了或消费了就去唤醒,万一根本就没人等待呢?所以记录一下等待的人数,如果有人等待就去唤醒,没人等待就不唤醒了

我们给阻塞队列中可以放任何东西,可以放一个对象,对象中存着任务,也可以放一个函数对象,让"消费者"去执行这个函数

相关推荐
古希腊掌管学习的神2 分钟前
[LeetCode-Python版]相向双指针——611. 有效三角形的个数
开发语言·python·leetcode
赵钰老师3 分钟前
【R语言遥感技术】“R+遥感”的水环境综合评价方法
开发语言·数据分析·r语言
雨中rain9 分钟前
Linux -- 从抢票逻辑理解线程互斥
linux·运维·c++
就爱学编程11 分钟前
重生之我在异世界学编程之C语言小项目:通讯录
c语言·开发语言·数据结构·算法
Bessssss29 分钟前
centos日志管理,xiao整理
linux·运维·centos
s_yellowfish29 分钟前
Linux服务器pm2 运行chatgpt-on-wechat,搭建微信群ai机器人
linux·服务器·chatgpt
豆是浪个31 分钟前
Linux(Centos 7.6)yum源配置
linux·运维·centos
vvw&32 分钟前
如何在 Ubuntu 22.04 上安装 Ansible 教程
linux·运维·服务器·ubuntu·开源·ansible·devops
我一定会有钱33 分钟前
【linux】NFS实验
linux·服务器
Oneforlove_twoforjob34 分钟前
【Java基础面试题025】什么是Java的Integer缓存池?
java·开发语言·缓存