多线程-生产者消费者模型,条件变量的使用,阻塞队列的实现

28 篇文章 1 订阅
17 篇文章 0 订阅
文章介绍了条件变量的概念及其在多线程中的应用,如pthread_cond的初始化、等待与唤醒操作。接着通过生产者消费者模型解释了条件变量如何用于线程间的同步与通信,展示了如何使用BlockQueue实现线程安全的阻塞队列,强调了在push和pop操作中使用while循环进行条件判断的重要性。
摘要由CSDN通过智能技术生成

目录

1.条件变量的概念

2.pthread_cond的使用

3.生产者消费者模型

4.基于BlockQueue的生产者消费者模型

BlockQueue.hpp

Task.hpp

test.cc

Q:为什么pthread_cond_wait的时候要传入一把锁?

Q:在阻塞队列的push和pop里面条件判断时,为什么要用while?


1.条件变量的概念

首先来认识一下什么是条件变量:

        当我们互斥的访问某个变量(临界资源)的时候,可能发现在其它线程改变状态之前,它什么也做不了(比如抢票,票数就是临界资源,当票数为0的时候,所有抢票的线程什么也做不了)

        一般而言,如果只有锁,我们不容易知道当前临界资源的状态,需要不停地去轮询尝试访问才能知道。比如抢票的线程,当票数为0的时候,可以暂时等待,而不是一直尝试抢票,如果一直抢票,抢票线程会一直获取锁,然后判断是否有票,没票,释放锁,不停循环这个操作,做无用功,而且占用锁。这个时候就需要条件变量,让它不要一直尝试,进入等待,满足某个条件以后,其他线程通过条件变量将抢票线程唤醒


2.pthread_cond的使用

        全称是pthread_condition,是pthread线程库(POSIX线程库)中的条件变量,使用方法和互斥锁相同,pthread_cond_t cond;定义一个全局的条件变量即可使用

  • 初始化

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;  //也可以使用静态分配的初始化的方式

参数:

cond:要初始化的条件变量

attr:NULL

  • 销毁

int pthread_cond_destroy(pthread_cond_t *cond)

  • 等待条件满足

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);

参数:

cond:要在这个条件变量上等待

mutex:互斥量,用于等待的时候释放锁,唤醒的时候重新竞争到锁

  • 唤醒等待

int pthread_cond_broadcast(pthread_cond_t *cond);  //一次唤醒所有在等待队列里面所有的线程(也是按照顺序唤醒)

int pthread_cond_signal(pthread_cond_t *cond);  //一次唤醒一个,唤醒在条件变量里面的等待队列里面等待的第一个线程


3.生产者消费者模型

什么是生产者和消费者模型?

        生产者生产,把资源放到一个公共区域(比如阻塞队列),然后消费者从公共区域中拿取资源,进行消费。消费者和消费者之间是竞争互斥的关系(对共享变量的操作,是互斥的);生产者和生产者是竞争互斥的关系,生产者和消费者之间也是互斥的关系,同时两者之间还需要同步,比如公共区域没有资源了,消费者就需要等待,生产者生产资源后,唤醒消费者去消费(即便你消费者消费地再快,你也必须等待生产者生产,这就是同步);后者同理。

  • 只有生产者知道,消费者什么时候应该来消费了;只有消费者知道,生产者应该什么时候来生产;这就可以使用条件变量来互相通知对方

生活中的超市就是生产者消费者模型,将生产环节和消费环节进行了解耦。

生产者和消费者同步:

        供货商和消费者之间需要同步,因为没货消费者就需要停止消费(等待,满足一定条件以后才能执行),货满了生产者就需要停止生产(等待,满足一定条件以后才能执行);在线程安全的前提下,线程需要按照一定的顺序执行(比如某个线程需要在满足某些条件以后才能执行)。

生产者消费者模型的优点

1.将生产者的生产环节和消费者消费环节进行了解耦。

2.支持并发操作(在同一个时间段,生产者和消费者都在执行)

3.对生产者和消费者进行了同步,就算生产者生产速度和消费者消费速度不匹配也不会出错。


4.基于BlockQueue的生产者消费者模型

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

        生产者生产一批任务,然后将这批任务放到阻塞队列里面;消费者把任务拿到自己的上下文当中,并且对任务进行处理。(假设任务是进行加减乘除计算)

BlockQueue.hpp

        首先实现阻塞队列,用一个互斥锁维护公共资源,也就是队列本身,两个条件变量用于当队列满的时候让生产者等待,当队列空的时候让消费者等待。将加锁解锁,等待唤醒等操作全部封装起来。

        在生产者Push添加任务的时候,要判断当前队列是否满,如果满了就让生产者等待,然后加锁对队列进行操作,最后唤醒消费者来消费队列中的任务。

        当消费者Pop取走任务的时候,同理要判断队列是否为空,如果空了就让消费者等待,然后加锁对队列进行操作,最后唤醒生产者来生产队列中的任务。

#pragma once

#include <iostream>
#include <queue>
#include <pthread.h>

namespace ns_blockqueue
{
    const int default_maxsize = 5;

    template <class T>
    class BlockQueue
    {
    private:
        std::queue<T> _bq;    // 阻塞队列
        int _maxsize;         // 队列的元素个数上限
        pthread_mutex_t _mtx; // 保护临界资源的锁
        // 1. 当生产满了的时候,就应该不要生产了(不要竞争锁了),而应该让消费者来消费
        // 2. 当消费空了,就不应该消费(不要竞争锁了),应该让生产者来进行生产
        pthread_cond_t _is_full;  // _bq满了, 消费者在该条件变量下等待
        pthread_cond_t _is_empty; // _bq空了,生产者在该条件变量下等待
    private:
        bool IsFull()
        {
            return _bq.size() == _maxsize;
        }
        bool IsEmpty()
        {
            return _bq.size() == 0;
        }
        void LockQueue()
        {
            pthread_mutex_lock(&_mtx);
        }
        void UnlockQueue()
        {
            pthread_mutex_unlock(&_mtx);
        }
        void ProducterWait()
        {
            // pthread_cond_wait
            // 1. 调用的时候,会先自动释放_mtx,然后再挂起自己
            // 2. 返回的时候,会先自动竞争锁,获取到锁之后,才能返回
            pthread_cond_wait(&_is_empty, &_mtx);
        }
        void ConsumerWait()
        {
            pthread_cond_wait(&_is_full, &_mtx);
        }
        void WakeupComsumer()
        {
            pthread_cond_signal(&_is_full);
        }
        void WakeupProducter()
        {
            pthread_cond_signal(&_is_empty);
        }

    public:
        BlockQueue(int cap = default_maxsize) : _maxsize(cap)
        {
            pthread_mutex_init(&_mtx, nullptr);
            pthread_cond_init(&_is_empty, nullptr);
            pthread_cond_init(&_is_full, nullptr);
        }
        ~BlockQueue()
        {
            pthread_mutex_destroy(&_mtx);
            pthread_cond_destroy(&_is_empty);
            pthread_cond_destroy(&_is_full);
        }

    public:
        void Push(const T &in)
        {
            LockQueue();
            while (IsFull())
            // 注意这里必须采用循环的方式不能用if,如果使用if,wait挂起失败仍然会继续向下执行
            {
                ProducterWait(); // 队列满了,不能继续生产,生产者等待
            }
            // 向队列中放数据,生产
            _bq.push(in);

            // if(_bq.size() > _maxsize/2 ) WakeupComsumer();
            UnlockQueue();
            WakeupComsumer();
        }

        void Pop(T *out)
        {
            LockQueue();
            // 从队列中拿数据,消费

            while (IsEmpty())
            {
                ConsumerWait(); // 队列空了,不能继续消费,生消费者等待
            }
            *out = _bq.front();
            _bq.pop();

            // if(_bq.size() < _maxsize/2 ) WakeupProducter();  //可以加上唤醒的条件
            UnlockQueue();
            WakeupProducter();
        }
    };
}

Task.hpp

        定义一个任务类,用于保存任务的数据以及处理方法,Run方法里面有任务的处理方法,生产者生产任务的时候将任务的数据存放进来,消费者消费的时候调用Run方法进行任务的处理。

#pragma once

#include <iostream>
#include <pthread.h>

namespace ns_task
{
    class Task
    {
    private:
        int x_;
        int y_;
        char op_; //+/*/%
    public:
    
        Task() {}
        Task(int x, int y, char op) : x_(x), y_(y), op_(op)
        {
        }
        int Run()
        {
            int res = 0;
            switch (op_)
            {
            case '+':
                res = x_ + y_;
                break;
            case '-':
                res = x_ - y_;
                break;
            case '*':
                res = x_ * y_;
                break;
            case '/':
                res = x_ / y_;
                break;
            case '%':
                res = x_ % y_;
                break;
            default:
                std::cout << "bug" << std::endl;
                break;
            }
            std::cout << "当前任务正在被: " << pthread_self() << " 处理: " \
            << x_ << op_ << y_ << "=" << res << std::endl;
            return res;
        }
        ~Task() {}
    };
}

test.cc

        创建生产者和消费者线程循环生产和消费Task任务,这里创建了1个生产者和5个消费者

#include "BlockQueue.hpp"
#include "Task.hpp"

#include <time.h>
#include <cstdlib>
#include <unistd.h>

using namespace ns_blockqueue;
using namespace ns_task;

void *consumer(void *args)
{
    BlockQueue<Task> *bq = (BlockQueue<Task> *)args;
    while (true)
    {
        Task t;
        bq->Pop(&t); // 消费一个任务,第一步是把任务从队列中取出来
        t.Run();     // 第二步是执行Task的run方法
    }
}

void *producter(void *args)
{
    BlockQueue<Task> *bq = (BlockQueue<Task> *)args;
    std::string ops = "+-*/%";
    while (true)
    {
        int x = rand() % 100 + 1; //[1,100]
        int y = rand() % 100 + 1; //[1,100]
        char op = ops[rand() % 5];
        Task t(x, y, op);
        std::cout << "生产者派发了一个任务: " << x << op << y << "=?" << std::endl;

        // 将数据推送到任务队列中
        bq->Push(t);
        sleep(1);
    }
}

int main()
{
    srand((long long)time(nullptr));
    BlockQueue<Task> *bq = new BlockQueue<Task>();

    pthread_t p;
    pthread_t c1, c2, c3, c4, c5;

    pthread_create(&c1, nullptr, consumer, (void *)bq);
    pthread_create(&c2, nullptr, consumer, (void *)bq);
    pthread_create(&c3, nullptr, consumer, (void *)bq);
    pthread_create(&c4, nullptr, consumer, (void *)bq);
    pthread_create(&c5, nullptr, consumer, (void *)bq);
    pthread_create(&p, nullptr, producter, (void *)bq);

    pthread_join(c1, nullptr);
    pthread_join(c2, nullptr);
    pthread_join(c3, nullptr);
    pthread_join(c4, nullptr);
    pthread_join(c5, nullptr);
    pthread_join(p, nullptr);

    return 0;
}

Q:为什么pthread_cond_wait的时候要传入一把锁?

        因为我们在调用pthread_cond_wait的时候,一般都是占用着锁的(一般都是检测到临界资源,发现不满足条件,才会调用这个wait方法),此时如果不把锁释放,直接wait的话,那就死锁了,其他线程也无法获取锁,修改这个临界资源。

        1.所以调用pthread_cond_wait的时候,传入当前线程持有的锁,会让当前线程先自动释放锁,然后再进入等待队列等待。

        2.因为被唤醒的线程会继续往下执行,而线程此时所处的状态是持有锁的状态,所以当线程被唤醒的时候,是需要保证线程继续执行的时候是处于持有锁的状态的。

Q:在阻塞队列的push和pop里面条件判断时,为什么要用while?

        如果wait挂起失败,或者唤醒的时候出现伪唤醒,此时还需要再次对条件进行判断。如果使用if,一旦出现上述错误,代码会继续执行,导致问题的发生,所以这里要用while循环,可以保证当循环退出的时候,一定是条件得到了满足。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值