Linux:线程同步(条件变量、生产者消费者模型,POSIX信号量)

条件变量
  • 基本概念
    我们之前说过,当线程互斥访问某个变量,可能会产生饥饿问题(例如一个自习室一次只能有一个人进入,当A这个人进入自习室之后,其他人就不能进入,这叫做互斥,但是比如A这个人突然不想占用自习室了,加锁然后出来,然后又改变主意又开锁进去,这样反反复复,那么后面排队的人就一直无法进入自习室,也就是A线程因为自己的优先级高而可以访问这个变量,但是在访问到变量后却将大部分的时间花在了开锁和加锁上,其他人产生了饥饿问题,此时就需要使用同步,就是在保证数据安全下,让线程能够按照特定顺序访问临界资源,从而避免饥饿问题,例如这里可以只要A这个人出了自习室,就按照顺序排到队伍后面等待,而同步就需要一种机制,这里使用到了条件变量)
    当一个线程互斥地访问某个变量时,他可能发现在其他线程改变状态之前,他什么也做不了,例如一个线程访问队列时,发现队列为空,他只能等待,直到其他进程将一个结点添加到队列中,这种情况需要条件变量。
    互斥量用于上锁,条件变量用于等待,条件变量和互斥量要一起使用,允许线程以无竞争方式等待特定的条件产生,条件变量本身是由互斥量保护的,线程在改变条件状态之前必须首先锁住互斥量,其他线程在获得互斥量之前不会察觉到这种改变,因为互斥量必须在锁定之后才能计算条件
  • 条件变量函数
    1、初始化
    函数原型是:
 int pthread_cond_init(pthread_cond_t *restrict cond,  const pthread_condattr_t *restrict attr);

其中参数cond表示要初始化的条件变量;attr默认为NULL
2、销毁

 int pthread_cond_destroy(pthread_cond_t *cond);

cond就是要销毁的条件变量
3、等待条件满足

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

其中参数cond表示要在这个条件变量上等待,mutex表示互斥量,它对条件变量进行保护,调用者把锁住的互斥量传给该函数,函数自动将调用线程放到等待条件的线程列表上,对互斥量进行解锁,因此这里传互斥量这个参数的作用就是:(1)自动解锁、(2)将其唤醒时重新获得锁
4、唤醒等待

#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

signal是唤醒一个,broadcast是唤醒一群。
例如实现代码:

#include<stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
pthread_mutex_t lock;
pthread_cond_t cond;
void *Wait(void* arg)
{
    while(1)
    {
        pthread_cond_wait(&cond,&lock);//没有被唤醒,就阻塞
        printf("active.......");
    }
}
void *Signal(void* arg)
{
    pthread_cond_signal(&cond);//唤醒等待,通知在该条件变量下等待的线程
    sleep(1);
}
int main()
{    
    pthread_t t1,t2;
    pthread_cond_init(&cond,NULL);//初始化条件变量
    pthread_mutex_init(&lock,NULL);//初始化互斥量;
    pthread_create(&t1,NULL,Wait,NULL);
    pthread_create(&t2,NULL,Signal,NULL);

    pthread_join(t1,NULL);
    pthread_join(t2,NULL);
    pthread_mutex_destroy(&lock);
    pthread_cond_destroy(&cond);
}

最终运行结果就是每隔一秒打印一次active…。

生产者消费者模型

例如一个超市,超市中的货物相当于计算机中的数据,消费者就是计算机中的消费者,生产者就是供货商,供货商之间是互斥关系(供货商相互竞争),也就是生产者之间是互斥关系,而消费者与消费者之间也是互斥关系,生产者与消费者之间是互斥与同步关系(因为只有供货商将货物放到货架上之后,消费者才能取货物),因此这个超市相当于临界区,生产者往临界区中放货物,消费者从临界区中拿资源。
能够生产和消费,在内存中有一段缓冲区(临界资源),在计算机世界中,能够进行生产和消费只能是进程或者线程(执行体),生产者和消费者只能是进程或者线程,交易场所由内存空间承担,货物就是数据;因此可以得出,A线程要和B线程进行数据交易,A线程将数据放到内存的中间缓冲区(临界资源)中,B线程从内存的中间缓冲区(临界资源)中拿数据,这是单生产者-单消费者模型,因此要实现同步需要条件变量
总结:也就是三种关系(生产者之间:互斥,消费者之间:互斥,生产者与消费者:同步与互斥),两类角色(生产者,消费者:进程或线程),一个交易场所(中间缓冲区);就是321原则(方便记忆),实现同步需要条件变量,实现互斥需要锁。
那么为何要使用生产者消费者模型?它是通过一个容器来解决生产者消费者的强耦合问题,生产者与消费者之间不直接通信,通过阻塞队列来进行通信,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列中取。阻塞队列相当于一个缓冲区。
生产者消费者模型的好处有:
(1)支持并发(消费者调用生产者,因为函数调用是同步的,在生产者没有返回数据单位前,消费者只能等待,浪费时间,使用生产者-消费者模型,他们是两个独立的并发主体,生产者不停地将数据放进缓冲区,不用考虑消费者的问题)
(2)解耦(加入生产者消费者是2个类,消费者直接调用生产者,就产生了耦合,生产者改变,消费者就会随之改变,如果两者之间有一个缓冲区,就降低了耦合度)
(2)支持忙闲不均(当生产者产生数据时快时慢,消费者来不及处理,可以将数据放在缓冲区,等生产者的速度慢下来,消费者慢慢处理)
例如实现基于阻塞队列的单生产者-单消费者模型(不用考虑生产者同类之间的关系和消费者同类之间的关系)
阻塞队列与普通队列的区别在于当队列为空时,从队列获取元素的操作将会被阻塞,直到队列被放入了元素;当队列满时,往队列中存放元素的操作也会被阻塞,直到有元素被从队列中取出。

#include <iostream>
#include <pthread.h>
#include <queue>
#include <time.h>
#include <stdlib.h>
#include <unistd.h>
#include <cstdlib>
using namespace std;
class BlockQueue
{
    private:
        queue<int> q;
        int cap;//容量
        pthread_mutex_t lock;
        pthread_cond_t c_cond;//消费者条件变量
        pthread_cond_t p_cond;//生产者条件变量
    private:
        void LockQueue()
        {
            pthread_mutex_lock(&lock);
        }
        void UnlockQueue()
        {
            pthread_mutex_unlock(&lock);
        }
        void ProducterWait()
        {
            cout<<"Producter wait"<<endl;
            pthread_cond_wait(&p_cond,&lock);
        }
        void ConsumerWait()
        {
            cout<<"Consumer wait"<<endl;
            pthread_cond_wait(&c_cond,&lock);
        }
        void SignalProducter()
        {
            cout<<"signal producter"<<endl;
            pthread_cond_signal(&p_cond);
        }
        void SignalConsumer()
        {
            cout<<"signal consumer"<<endl;
            pthread_cond_signal(&c_cond);
        }
        bool QueueIsFull()
        {
            return (q.size()==cap ? true:false);
        }
        bool QueueIsEmpty()
        {
            return (q.size()==0 ? true:false);
        }
    public:
        BlockQueue(int _cap =32)//构造函数
            :cap(_cap)
        {
            pthread_mutex_init(&lock,NULL);
            pthread_cond_init(&c_cond,NULL);
            pthread_cond_init(&p_cond,NULL);
        }
        ~BlockQueue()
        {
            pthread_mutex_destroy(&lock);
            pthread_cond_destroy(&c_cond);
            pthread_cond_destroy(&p_cond);
        }
        void PushData(const int& data)//生产者
        {
            LockQueue();//进行锁住
            if(QueueIsFull())//如果队列满,通知消费者,生产者进行等待
            {
                cout<<"queue is full"<<endl;
                SignalConsumer();
                ProducterWait();
            }
            q.push(data);//生产
            UnlockQueue();//解锁
        }
        void PopData(int &out)//消费者
        {
            LockQueue();
            if(QueueIsEmpty())
            {
                cout<<"queue is empty"<<endl;
                SignalProducter();
                ConsumerWait();
            }
            out=q.front();
            q.pop();
            UnlockQueue();
        }
};
void* Consumer(void* arg)
{
    int data;
    BlockQueue* bq=(BlockQueue*)arg;
    while(1)
    {
        bq->PopData(data);
        cout<<"consum data is :"<<data<<endl;
    }
}
void* Producter(void* arg)
{
    BlockQueue* bq=(BlockQueue*)arg;
    while(1)
    {
        int data=rand()%100+1;

        bq->PushData(data);
        cout<<"product data is :"<<data<<endl;
        sleep(1);//生产的慢一点
    }
}
int main()
{
    srand((unsigned long)time(NULL));
    BlockQueue *bq=new BlockQueue(4);
    pthread_t c,p;

    pthread_create(&p,NULL,Producter,(void*)bq);
    pthread_create(&c,NULL,Consumer,(void*)bq);

    pthread_join(p,NULL);
    pthread_join(c,NULL);
}

此时我们让生产者慢,消费者快,它的结果是:

[Daisy@localhost test_2019_11_7_2]$ ./cp
queue is empty
signal producter
Consumer wait
product data is :66
product data is :19
product data is :21
product data is :16
queue is full
signal consumer
Producter wait
consum data is :66
consum data is :19
consum data is :21
consum data is :16
queue is empty
signal producter
Consumer wait
product data is :93
product data is :29
product data is :86
product data is :75
queue is full
signal consumer

可以看出生产者生产慢,消费者很快消费完了,生产者生产了4个,队列满了,通知消费者,消费者消费四个,队列空了,通知生产者,如果让消费者慢呢(即让消费者sleep(1))?
结果是生产者很快就生产了,消费者缓慢消费。
生产者将数据放在缓冲区中,消费者从缓冲区拿数据,两者并行独立,耦合度低,假如一方的实现改变,不会影响另一方,支持忙闲不均。
那么如何实现多生产者和多消费者呢?
其实我们只要维护生产者之间的互斥关系,消费者之间的互斥关系,也就是加两把锁来维持,此时我们就可以实现四种模式:单生产者-单消费者、多生产者-单消费者、单生产者-多消费者、多生产者-多消费者,通过控制加锁解锁来实现。

POSIX信号量

POSIX信号量与System V信号量的作用相同,都是用于同步操作,达到无冲突的访问共享资源的目的,但POSIX可以用于线程间同步
可以将信号量当成一个计数器,当值为1时代表有一份资源可以使用,有人申请这份资源时,值-1,当值为0时,其他线程想继续申请这份资源,会被挂起放入等待队列。
注意在使用信号量接口也要记得链接-pthread

  • 初始化信号量
    函数原型是:
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);

其中参数sem就是信号量,pshared:0表示线程间共享,非0表示进程间共享;value表示信号量的初始值,
返回值代表信号量成功与否;

  • 销毁信号量
    函数原型是:
int sem_destroy(sem_t *sem);

sem表示信号量

  • 等待信号量
    函数原型是:
int sem_wait(sem_t *sem);

相当于信号量的p操作,将信号量的值-1

  • 发布信号量
    函数原型是:
int sem_post(sem_t *sem);

相当于信号量的v操作,将信号量的值+1,表示资源使用完毕,可以归还资源了。

  • 基于环形队列的单生产者-单消费者模型
    现在我们编写一个基于环形队列的单生产者-单消费者模型
    首先介绍环形队列是怎么用于实现生产者消费者的,如图:
    在这里插入图片描述
    生产者和消费者存在同步与互斥的关系,即两者不能访问同一块资源,要在生产者放完数据后,消费者才能从这块区域拿数据,因此在这个环形队列中,只有当生产者和消费者没有访问同一个下标的数据块,此时生产者和消费者才可以同时进行
    在当前下标的数据块时,进行生产(p)操作时,要判断已经生产过的是否消费了,没有的话,就会发生覆盖,同样,在进行消费(c)操作时,要判断之前消费的是否已经被生产了,如果没有生产,就是空的,无法进行消费,即一定要保证数据是有效的。
    环形队列使用数组模拟,使用模运算来模拟环状特性,由于环形队列起始状态与终止状态是一样的,不好判断空或者满,可以通过加计数器或者标记位来判断,也可以预留一个空的位置,作为满的状态。
    因此总结如下:
    (1)消费者要跟在生产者的后面,消费者不能超过生产者
    (2)生产者的速度不能超过消费者一圈,因为在放完一圈后只能等待消费者拿取数据
    (3)开始时生产者和消费者指向0下标处
    (4)将空格放满时,消费者没有拿时,生产者和消费者同时指向0下标处
    (5)使用两个信号量:空格数目的信号量和有数据的信号量
    (6)空格初始信号量为队列的容量,数据块的信号量初始值为0,当一个空格被放入数据时,空格信号量-1(执行P操作),数据块信号量+1(执行V操作);当一个数据被拿走时,空格信号量+1(执行V操作),数据块的信号量-1(执行P操作).
    (7)为空时,让生产者先运行,为满时,让消费者先运行。
    如图:
    在这里插入图片描述
    生产者先进行P操作拿空格资源,放入数据,然后进行V操作归还数据资源;消费者先进行P操作拿数据资源,然后进行V操作归还空格资源。
    实现代码如下:
#include<iostream>
using namespace std;
#include <vector>
#include <semaphore.h>
#include <pthread.h>
#include <time.h>
#include <stdlib.h>
#include <unistd.h>
class RingQueue
{
    private:
        vector<int> buf;
        int cap;//容量
        sem_t sem_blank;
        sem_t sem_data;
        int c_step;
        int p_step;
    private:
        void P(sem_t& s)//等待信号量
        {
            sem_wait(&s);
        }
        void V(sem_t &s)
        {
            sem_post(&s);//唤醒等待
        }
    public:
        RingQueue(int _cap=1024)
            :cap(_cap)
             ,buf(_cap)
        {
            c_step=p_step=0;
            sem_init(&sem_blank,0,cap);//空格一开始初始化的是cap个
            sem_init(&sem_data,0,0);
        }
        ~RingQueue()
        {
            sem_destroy(&sem_blank);
            sem_destroy(&sem_data);
        }
        //Consumer
        void PopData(int& data)
        {
            P(sem_data);//申请数据资源
            data=buf[c_step];
            c_step++;
            c_step%=cap;

            V(sem_blank);//归还空格资源
        }
        //Producter
        void PushData(const int& data)
        {
            P(sem_blank);//申请空格资源
            buf[p_step]=data;
            p_step++;
            p_step%=cap;

            V(sem_data);//归还数据资源
        }
};
void*consumerRun(void* arg)
{
    RingQueue* rq=(RingQueue*)arg;
    int data;
    while(1)
    {
        rq->PopData(data);
        cout<<"consume data is"<<data<<endl;
        sleep(data%5+1);
    }

}
void* producterRun(void* arg)
{
    RingQueue* rq=(RingQueue*) arg;
    while(1)
    {
        int data =rand()%100+1;
        rq->PushData(data);
        cout<<"product data is"<<data<<endl;
        //sleep(1);
       // sleep(data%5+1);
    }
}
int main()
{
    srand((unsigned long)time(NULL));
    RingQueue* rq=new RingQueue (8);
    pthread_t c,p;
    pthread_create(&c,NULL,consumerRun,(void*)rq);
    pthread_create(&p,NULL,producterRun,(void*)rq);
    pthread_join(c,NULL);
    pthread_join(p,NULL);
    delete rq;
}

其中我们定义了一个循环队列(用数组表示),定义两个信号量,即空格信号量和有数据的信号量,因为要取放数据,因此要定义两个下标,在放数据时,进行P操作(将空格资源拿到),然后放生产数据之后,进行V操作(将数据资源归还);在拿数据时,进行P操作(将数据资源拿到),然后消费数据之后,进行V操作(将空格资源归还)。
只有在空或者满时才有可能发生冲突,但是我们因为信号量机制保证了互斥,因此不会冲突
那么如果我们想要改为多生产者-多消费者模型,也只需要维护生产者之间的互斥关系,消费者之间的互斥关系,只需要加两把锁即可。
总结因此如果我们是基于阻塞队列来实现生产者-消费者,只能有一个角色进入临界资源进行生产或者消费行为,而基于环形队列的生产者-消费者,只有当两个角色指向同一个位置才需要进行互斥,但是大部分情况下两个角色不在同一个位置,因此两个角色可以同时进入,因此基于环形队列的生产者-消费者模型效率高
源代码:(github)
基于阻塞队列的单生产者-单消费者模型:https://github.com/wangbiy/Linux2/commit/a477d9ff338fca8149a663cecd3ec56f78068da1
基于环形队列的单生产者-单消费者模型:https://github.com/wangbiy/Linux3/commit/ae34f7bae12c5632e7307aa2be898d6c413fe475

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值