Linux--多线程(3)

1. 基于环形队列的生产消费者模型

1.1 环形队列的基本原理

对于生产者与消费者来说,要实现通信无非就是通过一块缓冲区来达到目的。

前面讲到了基于阻塞队列的生产消费模型实现:点击查看博客详情

该模型的原理是生产者或者消费者没有活动空间时(队列为满,生产者无法生产,直到消费者使得队列不为满;消费者同理),某一方通过阻塞队列进行阻塞式等待,直到达到了活动的条件(队列不为满或者队列不为空),才进行生产或者消费活动。

这是基于阻塞队列的生产消费模型。

这次要实现的是基于环形队列的生产消费模型。

首先实现单线程情况下,也即是单生产者单消费者的情况,其次进行加锁处理,得到多线程的情况。

多线程的实现意义,是能够进行环形队列的并发访问。

  • 基本原理
  1. 生产者和消费者开始的时候,指向的就是同一个位置;在队列为满的时候,也指向同一个位置。
  2. 队列为空的时候,应该让生产者先访问;队列为满的时候,应该让消费者先访问。

所以,当队列既不为空又不为满的时候,生产者和消费者一定指向的不是同一个位置。

那么我们就可以利用这个原理,在生产者和消费者不处于同一位置,那么久说明多个执行流访问的是临界资源的不同区域,就可以实现并发。 但是这个工作是程序员本身完成的,而不是信号量设置好的。

2. 基本实现思想

对于生产者来说,最关心的资源应该是环形队列中空的位置;对于消费者来说,最关心的资源应该是环形队列中有数据的位置,其实就是资源存在的位置。

在这个模型中,要遵守几个规则:

  1. 生产者不能把消费者套一个圈,也就是说最多生产一圈,消费者就要行动了。

  2. 消费者不能超过生产者

  3. 当指向同一个位置的时候,要根据空、满的状态来判定谁先执行

  4. 除此之外,生产和消费可以并发执行

对于生产者来说,我们申请格子资源,格子资源变少了,其实就是对格子资源做P操作,那么可以放的数据资源更多了,那此时也就相当于对数据资源做V操作;消费者同理。

所以可以通过释放对方资源的方式来达到数据交互的效果。

3. POSIX信号量

3.1 概念

信号量本质是一个计数器,申请了信号量以后,可以达到预定临界资源的效果。

POSIX信号量和SystemV信号量相同,都可以用于同步操作,达到无冲突访问共享资源的目的。但POSIX可以用于线程间同步。

在未加锁情况下,如果多个线程同时对临界资源进行访问操作,是极度不安全的,所以想要实现多个线程并发,需要有信号量的申请。

每个线程申请了信号量以后,相当于对一块临界资源进行了预定,并且将其分成一个个小资源,每个线程同时访问该临界资源不同区域,达到多线程并发的效果。

对于每个线程来说,想访问临界资源。都必须先申请信号量资源,这就涉及到了信号量的数目多少。

  • P、V操作

这个过程其实就是对信号量进行加加减减。申请信号量的过程有成功或者失败,成功就是count–,失败就是挂起等待,即P操作;释放信号量就是count++,即V操作。

  • PV操作的原子性实现

PV操作的原子性其实是对于其他PV操作而言的,即一个P或V操作不能被其他的PV操作给打断,即需要实现PV操作的互斥。那么我们可以把PV操作的代码当成临界区,保证只有一个进程能访问临界区即可。实现对临界区资源的互斥访问的方法有很多种,比如Peterson算法,禁用中断,加锁等等等

3.2 信号量的等待唤醒机制

P操作和V操作在信号量中实现通知和等待的机制。下面详细解释一下它们是如何工作的:

  1. P操作(相当于Wait):
  • 当一个线程尝试执行P操作时,它需要获取信号量。如果信号量的计数器大于零,表示有可用资源,线程可以立即继续执行并占用一个资源。在这种情况下,P操作成功。
  • 如果信号量的计数器为零,表示没有可用资源,线程需要等待。线程进入等待状态,从当前执行位置暂停执行,并且释放对该信号量的控制。这样,其他线程能够继续执行并尝试获取信号量。
  • 当某个线程执行V操作并释放一个资源时,等待的线程可以通过竞争获取信号量来继续执行。
  1. V操作(相当于Signal):
  • 当一个线程执行V操作时,它会将信号量的计数器加一,表示有一个资源释放出来。

  • 如果有其他线程正在等待(由于之前执行了P操作而被阻塞),等待的线程会被唤醒。唤醒的线程将再次尝试执行P操作来获取信号量,如果成功获取,就可以继续执行。

  • 信号量的优缺点

优点:简单,表达能力强,用PV操作可以解决多种类型的同步/互斥问题
缺点:不够安全,PV操作使用不当容易产生死锁,遇到复杂同步互斥问题实现复杂

4. 基于单线程的环形队列模型具体实现

4.1 初始化信号量

在这里插入图片描述

参数:

pshared:0表示线程间共享,非零表示进程间共享
value:期望申请的信号量初始值

4.2 申请信号量

功能:申请信号量,会将信号量的值减1

在这里插入图片描述

这是申请信号量的函数,是经典的P操作,在申请信号量的过程中有重要作用。

这里的申请和定义信号量类型的变量不一样,申请有失败的可能的,所以要进行等待判断。

4.3 发布信号量

功能:发布信号量,表示资源使用完毕,可以归还资源了,将信号量值加1

这个便是V操作,和V操作形成对比:

在这里插入图片描述

4.4 销毁信号量

信号量使用完以后放在析构函数进行销毁:

在这里插入图片描述

有了这些接口可以设置单线程的生产消费模型:

头文件:

#include <semaphore.h>
#include <vector>
namespace zcb
{
    const int g_cap = 5;
    template<class T>
    class RingQueue
    {
    private:
        int cap_;//队列初始大小
        std::vector<T> ring_queue_;
        
        //格子、数据资源的信号量
        sem_t blank_sem_;
        sem_t data_sem_;

        //生产者、消费者所处位置
        int p_step_;
        int c_step_;
    public:
        RingQueue(int cap = g_cap)
            :cap_(cap)
            ,ring_queue_(cap)
        {
            sem_init(&blank_sem_,0,cap);
            sem_init(&data_sem_,0,0);
            p_step_ = c_step_ = 0;
        }
        ~RingQueue()
        {
            sem_destroy(&blank_sem_);
            sem_destroy(&data_sem_);
        }
        void push(const T& in)
        {
            //使用格子,格子减少,就是对格子资源进行P操作
            sem_wait(&blank_sem_);//P
            ring_queue_[p_step_] = in;
            //生产插入了数据,数据增多,就是对数据资源做了V操作
            sem_post(&data_sem_);//V

            p_step_++; 
            p_step_ %= cap_;
        }
        //消费者同理
        void pop(T* out)
        {
            sem_wait(&data_sem_);
            *out = ring_queue_[c_step_];

            sem_post(&blank_sem_);
            c_step_++;
            c_step_ %= cap_;
        }

    };
}

源文件:

#include <iostream>
#include <pthread.h>
#include "task.hpp"
#include "ring_q.hpp"
#include <time.h>
#include <unistd.h>
using namespace zcb;
using namespace ring;
using std::cout;
using std::endl;
void* producter(void* arg)
{
    RingQueue<int>* pro = (RingQueue<int>*)arg;
    while(1)
    {
        int data = rand()%20+1;
        pro->push(data);
        cout<<"生产者生产数据是:"<< data << endl;
    }
}
void* comsumer(void* arg)
{
    RingQueue<int>* con = (RingQueue<int>*)arg;
    while(1)
    {
        sleep(1);
        int out = 0;
        con->pop(&out);
        cout << "消费者消费数据是:" << out << endl;
    }
}
int main()
{
    srand((long long)time(nullptr));
    pthread_t p,c;
    RingQueue<int>* r = new RingQueue<int>();
    //创建线程,让它们看到同一份缓冲区,也就是环形队列
    pthread_create(&p,nullptr,producter,(void*)r);
    pthread_create(&c,nullptr,comsumer,(void*)r);
		
	  //线程等待
    pthread_join(p,nullptr);
    pthread_join(c,nullptr);
    return 0;
}

3. 多生产多消费

如果想实现多生产者和多消费者同时工作,是必须要加锁的,而这个加锁的地方,是放在P操作之后。

生产者函数如下(消费者函数同理):

void push(const T& in)
{
   sem_wait(&blank_sem_);//P

   //申请完信号量以后再抢锁
   pthread_mutex_lock(&p_mutex_);
   ring_queue_[p_step_] = in;

   p_step_++; 
   p_step_ %= cap_;
   pthead_mutex_unlock(&p_mutex_);
   sem_post(&data_sem_);//V
}

举个例子:如果想要实现并发,如果将锁放在了P操作之前,就会造成PV操作之前都必须要申请到锁的情况,这其实和单线程没有区别,并且申请到了锁以后,PV操作如果不成功,还是需要从新来申请锁,所以效率也会更慢。

而放在P操作之后,可以理解为在申请锁之前,多个线程就已经申请到了信号量,即PV操作是成功的。那么此时只需要等待竞争锁就行了。

而这里正是多生产多消费的优势,每次都只能一个线程生产,一个线程消费,可以并发地获取和处理任务。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

久菜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值