【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要重新判断条件

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

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

相关推荐
微露清风3 分钟前
系统性学习C++-第五讲-内存管理
java·c++·学习
qiuiuiu4138 分钟前
正点原子RK3568学习日志-编译第一个驱动程序helloworld
linux·c语言·开发语言·单片机
周之鸥22 分钟前
从零部署 Astro 静态网站到云服务器(含 HTTPS 一键配置)
运维·服务器·ubuntu·http·https·astro
林开落L29 分钟前
线程进阶:线程池、单例模式与线程安全深度解析
linux·安全·单例模式·线程池
Microsoft Word34 分钟前
跨平台向量库:Linux & Windows 上一条龙部署 PostgreSQL 向量扩展
linux·windows·postgresql
molong9311 小时前
Kotlin 内联函数、高阶函数、扩展函数
android·开发语言·kotlin
盼哥PyAI实验室1 小时前
踏上编程征程,与 Python 共舞
开发语言·python
noravinsc1 小时前
centos如何做的时间同步
linux·运维·centos
阿无,1 小时前
Java设计模式之工厂模式
java·开发语言·设计模式
星夜钢琴手1 小时前
推荐的 Visual Studio 2026 Insider C++ 程序项目属性配置
c++·visual studio