Linux_线程同步(条件变量(pthread_cond_t)实现线程同步_POSIX信号量sem_t实现线程同步互斥_生产者消费者模型(三种关系,两种角色,一个交易场所)阻塞队列模型_环形队列模型)

1.线程同步的重要性(避免线程饥饿问题)

根据线程互斥中可知,为了保护临界资源提出了互斥锁的概念。根据线程互斥中的案例代码可知:单纯加锁也会导致一直是一个线程访问临界资源的问题。
因为线程刚刚将锁释放,这个线程对于锁的竞争比其他线程更强,所以可能存在一个线程使得cout减少到0,这时需要引入同步机制,让每个线程都有机会使得cout-- 。

单纯加锁使个别线程竞争力很强导致其他线程的饥饿问题。

2.线程同步的定义

线程同步:保证在数据安全的前提下(线程互斥),让多执行流访问资源具有一定的顺序性,高效的使用临界资源。

3.条件变量(pthread_cond_t)

条件变量:对临界资源是否就绪的一种数据化描述。
条件变量不保护临界资源,通常要配合mutex锁一起使用

在这里插入图片描述

初始化条件变量(pthread_cond_init(pthread.h))

在这里插入图片描述
两种初始化条件变量的方法
1.调用函数
2.cond=PTHREAD_COND_INITIALIZER初始化
参数解释:
cond:初始化的条件变量
attr:条件变量的属性,一般为空

销毁条件变量(pthread_cond_destroy(pthread.h))

在这里插入图片描述
参数解释:
cond:要销毁的条件变量

等待条件满足(pthread_cond_wait(pthread.h))

在这里插入图片描述
参数解释:
cond:在这个条件变量下等待
mutex:互斥锁
注意:每一次调用这个函数都是从不满足条件开始。

pthread_cond_wait函数需要传递锁的原因:
调用这个函数时这个线程往往是已经申请到锁资源的,在临界区,不能直接挂起,否则会发生死锁。

所以第二个参数的意思是:当因为某些条件不满足时需要等待,调用这个函数时会自动将线程申请到的锁资源释放
如果当前等待线程被唤醒后,这个线程会自动获得mutex锁

唤醒等待线程(pthread_cond_signal(pthread.h))

在这里插入图片描述
参数解释:
cond:在这个条件变量下唤醒

注意:
pthread_cond_signal:唤醒条件变量下的一个线程
pthread_cond_broadcast:唤醒条件变量下的所有线程

demo1用主线程控制其他线程排队式轮循

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

pthread_mutex_t lock;

pthread_cond_t cond;

void*Run(void*arg)
{
  pthread_detach(pthread_self());
  char*Str=(char*)arg;
  std::cout<<Str<<" Run"<<std::endl;
  while(1)
  {
    pthread_cond_wait(&cond,&lock);//默认不满足条件,线程会阻塞等待条件变量就绪
    std::cout<<Str<<" open tid="<<pthread_self()<<std::endl;
  }
}

int main()
{
  pthread_mutex_init(&lock,nullptr);//初始化锁
  pthread_cond_init(&cond,nullptr);//初始化条件变量
  pthread_t tid1,tid2,tid3;
  pthread_create(&tid1,nullptr,Run,(void*)"tid1");
  pthread_create(&tid2,nullptr,Run,(void*)"tid2");
  pthread_create(&tid3,nullptr,Run,(void*)"tid3");

  while(1)
  {
    getchar();
    pthread_cond_signal(&cond);//唤醒线程
  }

  pthread_mutex_destroy(&lock);
  pthread_cond_destroy(&cond);
  return 0;
}

三个线程进入临界区时调用pthread_cond_wait阻塞,并且等待队列在条件变量中

主线程按下任意键后执行pthread_cond_signal,条件就绪。

条件变量中的等待队列中队列头的线程被唤醒打印,之后循环又调用了pthread_cond_wait阻塞。这个线程被挂到条件变量等待队列末尾。

主线程再按下任意键后执行pthread_cond_signal,条件又一次就绪。这时条件变量中的等待队列中队列头的线程被唤醒打印。依次进行达到主线程排队式控制线程

运行结果
在这里插入图片描述

4.生产者消费者模型(三种关系,两种角色,一个交易场所)

三种关系

1.生产者和生产者 2.消费者和消费者 3.生产者与消费者

消费者与消费者线程竞争访问资源,是竞争关系(互斥关系,需要用锁保护)

生产者与生产者线程竞争生产资源,也是竞争关系(互斥关系,需要用锁保护)

生产者与消费者线程同样也是竞争关系同时也有同步关系。竞争:生产者与消费者竞争交易场所 同步:先进行生产在进行消费(需要用锁保护和用条件变量保持同步关系)

两种角色

生产者,消费者(指的是特定的进程与线程扮演)

一个交易场所

生产者要将数据生产到交易场所中,消费者处理数据从交易场所中拿取。交易的场所一般是内存中的缓冲区(eg:数组,链表,队列)

生产线程与消费线程通过缓冲区解耦合。支持并发

demo2基于阻塞队列的单生产者单消费者模型完成计算任务(管道)

在这里插入图片描述
Block_Queue.h(阻塞队列)

#pragma once

#include<iostream>
#include<pthread.h>
#include<queue>
#define MAX_SIZE 10

template<class T>
class BlockQueue
{
  public:
    BlockQueue(int _cap=MAX_SIZE):Capcity(_cap)
    {
      pthread_mutex_init(&lock,nullptr);
      pthread_cond_init(&full,nullptr);
      pthread_cond_init(&empty,nullptr);
    }

    bool IsFull(){return Queue.size()==Capcity;}

    bool IsEmpty(){return Queue.empty();}

    void PushDate(const T&Val){//生产者调用
      pthread_mutex_lock(&lock);//加锁
      while(IsFull()){//等待消费者消费
        pthread_cond_wait(&full,&lock);
      }
      Queue.push(Val);
      //唤醒消费者线程。
      if(Queue.size()>=Capcity/2){
        std::cout<<"数据充足,唤醒消费者"<<std::endl;
        pthread_cond_signal(&empty);
      }
      pthread_mutex_unlock(&lock);
    }

    void PopDate(T&Out){//消费者调用
      pthread_mutex_lock(&lock);
      while(IsEmpty()){//等待生产者生产
        pthread_cond_wait(&empty,&lock);
      }
      Out=Queue.front();
      Queue.pop();
      //唤醒生产者
      if(Queue.size()<=Capcity/2){
        std::cout<<"数据不足,唤醒生产者"<<std::endl;
        pthread_cond_signal(&full);
      }
      pthread_mutex_unlock(&lock);
    }

    ~BlockQueue(){
      pthread_mutex_destroy(&lock);
      pthread_cond_destroy(&full);
      pthread_cond_destroy(&empty);
    }
  private:
    std::queue<T>Queue;//临界资源
    int Capcity;
    pthread_mutex_t lock;//互斥关系
    pthread_cond_t full;//描述队列满
    pthread_cond_t empty;//描述队列空
};


task.h(任务)

#pragma once

#include<iostream>

class Task
{
  public:
    Task(){}
    Task(int _x,int _y,int _z):x(_x),y(_y),ch(_z)
  {}
    int Calculate()
    {
      int ret=0;
      switch(ch){
        case '+':
          ret=x+y;
          break;
        case '-':
          ret=x-y;
          break;
        case '/':{
            if(y==0) {
              std::cout<<"erro"<<std::endl;
              ret=-1;
            }
            else {
              ret=x/y;
            }
            break;
          }
        case '*':
          ret=x*y;
          break;
      }
      std::cout<<x<<ch<<y<<"="<<ret<<std::endl;;
      return ret;
    }
  private:
    int x;int y;char ch;
};

model.cpp(主函数)

#include"Block_Queue.h"
#include<cstdlib>
#include<ctime>
#include<unistd.h>
#include"task.h"

void*Product(void*arg)
{
  BlockQueue<Task>*Queue=(BlockQueue<Task>*)arg;
  const char*Str="+-*/";
  while(true){
    int x=rand()%100+1;
    int y=rand()%50;
    char ch=Str[rand()%4];
    Task task(x,y,ch);
    Queue->PushDate(task);//生产数据
    std::cout<<"生产数据:"<<std::endl;
  }
}

void*ConSum(void*arg)
{
  BlockQueue<Task>*Queue=(BlockQueue<Task>*)arg;
  while(true){
    Task task;
    Queue->PopDate(task);//取出数据
    std::cout<<"处理数据:"<<std::endl;
    task.Calculate();
    sleep(1);
  }
}


int main()
{
  srand(time(nullptr));
  pthread_t Pro,Con;
  BlockQueue<Task>*buff=new BlockQueue<Task>();
  pthread_create(&Pro,nullptr,Product,(void*)buff);
  pthread_create(&Con,nullptr,ConSum,(void*)buff);
  pthread_join(Pro,nullptr);
  pthread_join(Con,nullptr);
  return 0;
}

注意上述代码中使用while(IsFull())和while(IsEmpty())的原因:
1.防止pthread_cond_wait函数调用失败导致错误。
2.防止单生产者多消费者这种情况下,生产线程生产一个数据后,pthread_cond_broadcast唤醒条件变量下的所有线程,所有线程可能会在队列为空时消费数据导致崩溃。

使用while循环判断条件就绪,当任意线程被唤醒时要重新判断一次唤醒条件是否满足,保证了上面第二种情况不会出现

运行结果
在这里插入图片描述
阻塞队列体现在:当队列为空时消费线程挂起等待。队列满时生产线程挂起等待。这点与管道类似,管道文件自带同步互斥原理。

5.POSIX信号量(sem_t)

在实现进程间通信使用的是SystemV共享内存,Linux SystemV通信包括:共享内存,消息队列,信号量(SystemV信号量)
Linux_System V进程通信_共享内存

信号量本质(更细粒度的管理临界资源)

信号量的本质是一个计数器,这个计数器描述了临界资源中资源的数目的计数器。

条件变量实现线程同步时并不关心临界资源内的资源数目,其关注临界资源是否符合某种条件

信号量本质是一个临界资源的计数器,是一种更细粒度的临界资源管理方式

信号量的P(申请)V(释放)操作

申请信号量本质:信号量计数器- -,代表资源数减少1 P操作
释放信号量本质:信号量计数器++,代表资源数增加1 V操作

注意:

  • 线程要访问临界资源时首先要申请信号量,所以信号量也是临界资源的一种,所以信号量的PV操作也是原子的(与mutex互斥锁类似)。

所以信号量不仅仅只有计数器。还有保障原子性的mutex互斥锁和与条件变量类似的队列
在这里插入图片描述
队列链接的是要调度的线程,用双链表的形式组织

信号量初始化(sem_init(semaphore.h))

在这里插入图片描述
参数解释:
sem:要初始化的信号量
pshared:0表示线程间共享,非0表示进程间共享(基本不用)
value:信号量的值(计数器的初始值)

信号量的销毁(sem_destroy(semaphore.h))

在这里插入图片描述
sem:要销毁的信号量

等待信号量(sem_wait(semaphore.h)将信号量的值-1P操作)

在这里插入图片描述

发布信号量(sem_post(semaphore.h)将信号量的值+1V操作)

在这里插入图片描述

二元信号量(信号量的value值为1)

初始化信号量时初始化信号量的值设置为1,这时的信号量为二元信号量,基本等价于互斥锁

demo3利用二元信号模拟互斥锁完成互斥操作(不引入同步)

不引入同步根据上文可知会导致一个线程连续使cout- -类似互斥锁

#include<iostream>
#include<semaphore.h>
#include<pthread.h>
#include<unistd.h>
class Sem//封装信号量
{
  public:
    Sem(int Num)
    {
      sem_init(&sem,0,Num);
    }

    void P()//使计数器--
    {
      sem_wait(&sem);
    }

    void V()//使计数器++
    {
      sem_post(&sem);
    }

    ~Sem()
    {
      sem_destroy(&sem);
    }
  private:
    sem_t sem;
};

Sem sem(1);//二元信号量

int cout=1000;

void*Run(void*arg)
{
  char*Str=(char*)arg;
  while(true)
  {
    sem.P();
    if(cout>0){
      usleep(1000);
      cout--;
      std::cout<<Str<<"make cout-- cout="<<cout<<std::endl;
      sem.V();
    }
    else{
      sem.V();
      break;
    }
  }
  std::cout<<Str<<"quit"<<std::endl;
  pthread_exit((void*)1);
}

int main()
{
  pthread_t tid1,tid2,tid3;
  pthread_create(&tid1,nullptr,Run,(void*)"pthread1");
  pthread_create(&tid2,nullptr,Run,(void*)"pthread2");
  pthread_create(&tid3,nullptr,Run,(void*)"pthread3");

  pthread_join(tid1,nullptr);
  pthread_join(tid2,nullptr);
  pthread_join(tid3,nullptr);
  return 0;
}

运行结果:
在这里插入图片描述

demo4利用POSIX信号量实现无锁的环形单生产者单消费者模型

在这里插入图片描述
根据上图可知需要遵守的原则为:

  • 生产者于消费者指向同一个位置时要串行执行,生产者比消费者先执行。当生产者和消费者不指向同一个位置时可以并发执行
  • 当生产者线程追上消费者线程后,生产者线程要等待消费者线程消费。反之也是如此

Ring.h

#pragma once 

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

#define SIZE 32

template<class T>
class RingQueue
{
  private:
    std::vector<T>q;
    int Cap;

    int b_pos;//只有生产者线程会访问,单生产者不需要保护这个变量,下面也是如此
    int d_pos;

    sem_t block_Num;
    sem_t data_Num;
  private:
    void P(sem_t&s)
    {
      sem_wait(&s);
    }
    void V(sem_t&s)
    {
      sem_post(&s);
    }
  public:
    RingQueue(int cap=SIZE):Cap(cap),b_pos(0),d_pos(0)
    {
      q.resize(Cap);
      sem_init(&block_Num,0,Cap);//开始空间信号量为Cap
      sem_init(&data_Num,0,0);//开始数据信号量为0
    }
    
    void Push(const T&in)//生产数据
    {
      P(block_Num);//关心空间信号量,申请空间信号量,P操作使空间信号量-1
      q[b_pos]=in;
      V(data_Num);//生产者生产了一个数据,数据信号量,V操作使数据信号量+1
      b_pos++;
      b_pos%=Cap;
    }

    void Pop(T&out)
    {
      P(data_Num);//当数据信号量为0时,消费者线程挂起等待,所以一定是生产者线程先运行,消费者线程后运行
      out=q[d_pos];
      V(block_Num);//消费了数据,所以空间信号量V操作使空间信号量+1
      d_pos++;
      d_pos%=Cap;
    }

    ~RingQueue(){
      sem_destroy(&block_Num);
      sem_destroy(&data_Num);
    }
};

main.cpp

#include"Ring.h"
#include<unistd.h>
#include<stdlib.h>

void*Consumer(void*arg)
{
  RingQueue<int>*rq=(RingQueue<int>*)arg;
  while(true)
  {
    sleep(2);
    int x=0;
    rq->Pop(x);
    std::cout<<"Consumer End Num="<<x<<std::endl;

  }
}

void*Product(void*arg)
{
  RingQueue<int>*rq=(RingQueue<int>*)arg;
  while(true)
  {
    sleep(1);
    int Num=rand()%100+1;
    rq->Push(Num);
    std::cout<<"Product End Num="<<Num<<std::endl;
  }
}

int main()
{
  srand((unsigned int)time(nullptr));
  RingQueue<int>*rq=new RingQueue<int>();
  pthread_t c_tid,p_tid;
  pthread_create(&c_tid,nullptr,Consumer,rq);
  pthread_create(&p_tid,nullptr,Product,rq);

  pthread_join(c_tid,nullptr);
  pthread_join(p_tid,nullptr);
  return 0;
}

运行结果:
在这里插入图片描述
当队列满时,空间信号量为0,生产者线程挂起。当队列为空时,数据信号量为0,消费者线程挂起。所以不会发生数据的冲突覆盖

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

NUC_Dodamce

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

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

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

打赏作者

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

抵扣说明:

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

余额充值