Linux:详解多线程(线程同步、信号量和生产者与消费者模型的实现)(三)


本篇的内容和我之前写的Linux:详解多线程(线程安全、互斥和死锁)(二)是连在一起的,可以先看完这篇文章,再过来阅读本篇文章。

1. 同步

1.1 前言

我们在上一篇文章中实现了线程的互斥,保证了在一个线程对临界资源进行访问的时候,其他线程是不能对该临界资源进行任何操作的,但是这样还是有一些问题存在的,比如由于是循环的加锁解锁,这些临界资源又可能每次都会被同一个线程所拿到,而其他的线程获取不到该临界资源,这就造成了程序结果的不合理性。

举个例子来说把,假设现在桌子上有一碗饭(临界资源),同时呢,又存在两个人,一个人只负责吃饭(线程A),一个人只负责做饭(线程B),那么,如果在只是实现了互斥的情况下,即每次只能有一个人对这碗饭进行操作,那么就像刚刚上面的分析一样,又可能每次都是做饭的人一直做饭,而吃饭的人不会进行吃,那么碗里的饭就会一直增加。

但是这并不符合我们日常生活的逻辑,按理说,在碗里还有饭的时候,我们是不能够进行做饭的,因为只有一个碗,我们不可能将两份饭放到一个碗中,我们只能等吃饭的人把碗里的饭吃了之后,再将做的饭放入碗中,这样才就保证了吃饭做饭的合理性。那么,我们本篇文章要讲的同步就是用来解决这种问题的。

具体一点就是,同步的作用为:让多个执行流在访问临界资源的时候是合理访问的

1.2 条件变量

我们通常使用条件变量来实现同步。

条件的变量的本质是一个PCB等待队列再加上一堆接口

PCB等待队列:当线程发现资源不可用的时候,就调用条件变量的等待接口,将自己放到PCB等待队列当中,等待被程序唤醒。

PS:我们一定要明白在多线程的情况下,各个线程是并行运行的。

1.2.1 条件变量的初始化

初始化条件变量也分为两种情况,静态初始化动态初始化
我们可以使用man pthread_cond_init来查询相应的条件变量初始化。

静态初始化

pthread_cond_t cond = PTHREAD_COND_INITIALIZER

和互斥锁一样,他也是一个宏定义。
在这里插入图片描述
同理,它是不用自己主动释放资源的,是会由操作系统自动回收的。

动态初始化

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

参数:

  • cond:待初始化的"条件变量"的变量,一般情况下,均是传入一个pthread_cond_t的类型变量的地址
  • attr:条件变量的属性,一般情况下都是传入一个NULL,采取默认的属性。

返回值:

  • 0 : 表示成功
  • <0:表示失败

条件变量接口的返回值均是如此,下面就不再对其进行声明了。

1.2.2 条件变量的等待接口

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

参数:

  • cond:需要进行等待的条件变量的接口
  • mutex:是一个互斥锁变量,它的作用是如果一个执行流调用了该接口,就会将执行流对应的PCB放到参数为cond的PCB等待队列当中。

那么问题来了:

① 为什么需要互斥锁?
② 在调用该接口的时候,pthread_cond_wait函数的实现逻辑?
③ 如果一个线程在等待的时候,当它被唤醒之后,需要做什么事情?

解答:

① 为什么需要互斥锁?

传递互斥锁的原因是需要在pthread_cond_wait函数内部进行解锁,在解锁之后,其他执行流就可以获得这把互斥锁。
否则,如果调用pthread_cond_wait在进行等待的时候,不释放互斥锁,那么其他线程也不会获取到这个互斥锁,整个程序就无法正常的向前运行,换句话说就是我拿着锁进入了等待吃饭的队列中,而做饭的人需要获取我手中的这把锁从而进行做饭,但是现在它拿不到我手中的锁,因此,就会一直循环的申请拿锁却拿不到,程序就会一直卡住。

② 在调用该接口的时候,pthread_cond_wait函数的实现逻辑?

有三个步骤:

① 放入PCB等待队列中
② 释放互斥锁
③ 等待被唤醒

注意这里的①和②的顺序是不能乱的,如果为先释放互斥锁,那么有可能在线程A释放的一瞬间,并且还没有入队的时候,发生了线程切换,B拿到了这把互斥锁,然后B去进行吃饭,在吃完之后,进行解锁并通知PCB等待队列进行做饭,但此时PCB等待队列中并没有等待做饭的线程A,然后B再次拿到锁,发现碗中没有饭,然后就进入PCB等待队列中等待被唤醒,这个时候线程A被切换进来,然后线程A执行入队操作,最终线程A和线程B都在PCB等待队列中,程序也就无法正常的运行的了。因此,这里的顺序是不能乱的。

③ 如果一个线程在等待的时候,当它被唤醒之后,需要做什么事情?

会做两件事情:

① 先移出PCB等待队列
② 再抢互斥锁

注意这里再抢锁的时候,若是抢到了,那么pthread_cond_wait函数也就返回了,若是没有抢到,该函数就不会返回,而是一直等待的抢锁。

扩展:条件变量的等待接口中也有pthread_cond_timedwait,即带有超时时间的接口

1.2.3 条件变量的唤醒接口

int pthread_cond_signal(pthread_cond_t *cond);

参数:

  • cond:传入待唤醒的条件变量的地址

作用

通知(唤醒)PCB等待队列中的线程,如果被通知(被唤醒)的线程接收到了,则就会从PCB等待队列中进行出队操作,正常的执行代码。若是PCB等待队列中有多个线程,则至少随机唤醒一个PCB等待队列中的线程。

int pthread_cond_broadcast(pthread_cond_t *cond);

参数

  • cond:传入待唤醒的条件变量的地址

作用

pthread_cond_broadcast的作用和pthread_cond_signal的作用基本一样,但有一点不同的是,如果PCB等待队列中有多个线程,则他会唤醒所有的PCB等待队列中的线程。

1.2.4 条件变量的销毁接口

int pthread_cond_destroy(pthread_cond_t *cond);

参数

  • cond:传入待销毁的条件变量的地址

1.2.5 条件变量的实战探索

现在我们来继续进行前言中提到的场景,有一个碗和两个人,首先我们应该保证互斥,即同一时间中只有一个人能对碗进行操作,其次我们还应该保证碗中资源的合理性,即碗中只能有0和1两种状态,0代表碗是空的,1代表碗是满的,这点我们使用条件变量来对其进行实现。

代码如下:

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

#define PTHREADNUM 1

int bowl = 1;

pthread_mutex_t g_lock;
pthread_cond_t g_cond;

//只负责吃的线程
void* MyPthreadEat(void* arg)
{
    while(1)
    {
        pthread_mutex_lock(&g_lock);

        if(bowl < 1)
        {
            pthread_cond_wait(&g_cond,&g_lock);
        }
        //走到这说明碗中现在是有饭的
        --bowl;
        printf("i eat %d,i am %p\n",bowl,pthread_self());

        //在操作完之后,需要对负责做的PCB等待队列发出信号,
        //唤醒正在等待通知的做的线程
        pthread_cond_signal(&g_cond);
        pthread_mutex_unlock(&g_lock);
    }
    return NULL;
}

//只负责做的线程
void* MyPthreadMake(void* arg)
{
    while(1)
    {
        pthread_mutex_lock(&g_lock);

        if(bowl >= 1)
        {
            pthread_cond_wait(&g_cond,&g_lock);
        }
        //走到这说明碗中现在是没饭的
        ++bowl;
        printf("i make %d,i am %p\n",bowl,pthread_self());

        //在操作完之后,需要对负责吃的PCB等待队列发出信号,
        //唤醒正在等待通知的吃的线程
        pthread_cond_signal(&g_cond);
        pthread_mutex_unlock(&g_lock);
    }
    
    return NULL;
}

int main()
{
    pthread_mutex_init(&g_lock,NULL);
    pthread_cond_init(&g_cond,NULL);

    pthread_t tid_A[PTHREADNUM] , tid_B[PTHREADNUM];

    for(int i = 0;i < PTHREADNUM; ++i)
    {
        int ret = pthread_create(&tid_A[i],NULL,MyPthreadEat,NULL);
        if(ret < 0)
        {
            perror("pthread_create");
            return 0;
        }
        ret = pthread_create(&tid_B[i],NULL,MyPthreadMake,NULL);
        if(ret < 0)
        {
            perror("pthread_create");
            return 0;
        }    
    }

    for(int i = 0; i < PTHREADNUM; ++i)
    {
        pthread_join(tid_A[i],NULL);
        pthread_join(tid_B[i],NULL);
    }
    pthread_mutex_destroy(&g_lock);
    pthread_cond_destroy(&g_cond);
    return 0;
}

结果验证:

由于产生的结果过多,我们将其重定向到1.txt中进查看
在这里插入图片描述
在这里插入图片描述
当只有一个线程负责吃,一个线程负责做,这样做好像成功了,因为既实现了互斥,又保证了对碗资源访问的合理性,但是,若是有两个线程负责吃,两个线程负责做呢?
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
看看结果,发现结果又不对了,这只有一个碗,但是却出现了做了四碗饭的情况,也出现了吃了四碗饭的情况,甚至都有-1出现了,因此,这样做肯定是不对的,我们还需要对其进行改进。

我们看到,只有一个碗,但是却出现了不合理的情况,我们此时再回想一下,pthread_cond_wait函数被唤醒之后,会做什么事情:① 先移出PCB等待队列,② 抢互斥锁。然后我们再来看看有pthread_cond_wait函数的两个语句。

在这里插入图片描述
在这里插入图片描述
是了,假设你在吃完饭之后,唤醒等待做饭的线程,由于存在两个做饭的线程,可能有一个线程MakeA在PCB等待队列中,而另外一个MakeB阻塞在拿锁的阶段,当吃饭的线程刚释放掉这把互斥锁,MakeB就拿到了这把锁,然后做饭,最后释放锁(此时bowl = 1),由于是while循环,就会再上去进行抢锁,那么现在假设是MakeA抢到了这把互斥锁,由于没有对其进行判断,因此,MakeA会直接对碗中的资源进++操作,(此时bowl=2),这样就造成了资源的不合理性。

那么如何进行修改呢?我们只需将if改为while即可,让被唤醒后的线程再进行判断即可,这样就可以保证资源的合理性。

代码改进

//只负责吃的线程
void* MyPthreadEat(void* arg)
{
    while(1)
    {
        pthread_mutex_lock(&g_lock);

        while(bowl < 1)
        {
            pthread_cond_wait(&g_cond,&g_lock);
        }
        //走到这说明碗中现在是有饭的
        --bowl;
        printf("i eat %d,i am %p\n",bowl,pthread_self());

        //在操作完之后,需要对负责做的PCB等待队列发出信号,
        //唤醒正在等待通知的做的线程
        pthread_cond_signal(&g_cond);
        pthread_mutex_unlock(&g_lock);
    }
    return NULL;
}

//只负责做的线程
void* MyPthreadMake(void* arg)
{
    while(1)
    {
        pthread_mutex_lock(&g_lock);

        while(bowl >= 1)
        {
            pthread_cond_wait(&g_cond,&g_lock);
        }
        //走到这说明碗中现在是没有饭的
        ++bowl;
        printf("i make %d,i am %p\n",bowl,pthread_self());

        //在操作完之后,需要对负责吃的PCB等待队列发出信号,
        //唤醒正在等待通知的吃的线程
        pthread_cond_signal(&g_cond);
        pthread_mutex_unlock(&g_lock);
    }
    
    return NULL;
}

运行结果

在这里插入图片描述
虽然说好像还有问题,但是我们能看见的是,至少没有再出现刚刚那种资源访问不合理的情况了,但是为什么会卡死呢?我们紧接着对其进行分析。

我们使用ps aux | grep xxx的命令来查看当前进程的进程号,再使用pstack [pid]的命名擦好看当前进程的调用堆栈信息
在这里插入图片描述
发现四个工作线程均阻塞在了pthread_cond_wait函数内部。那这是为什么呢?

总结一下前面的我们可以发现,当我们在使用的时候只有一个条件变量,若是吃和做均只有一个线程那还好,每次唤醒,都可将互斥的那个线程唤醒,程序这样是没问题的。

但是若吃饭和做饭的人有多个的话,一个条件变量就会造成混乱,因为当你对碗操作完之后,你不知道唤醒的是PCB等待队列中的哪一个,(本次代码使用的是pthread_cond_signal函数,当PCB等待队列中有多个等待的线程的时候,至少会随机唤醒一个)。

因此,可能会存在这样的一个情况:一个负责吃的线程正在吃饭,而剩余的其他线程均在PCB等待队列中进行等待,当该线程吃完之后,pthread_cond_signal函数随机的唤醒了另一个负责吃的线程,然后该线程发现碗中没有饭,就又进入PCB等待队列等待被唤醒,由于没有唤醒负责做饭的线程出来做饭,因此,在外面的负责吃的线程均进入了PCB等待队列等待被唤醒,因此,四个线程均会被阻塞在pthread_cond_wait函数里面。

解决办法:

① 使用pthread_cond_broadcast函数,该函数会将PCB等待队列中的所有等待线程一次性全部唤醒
② 使用两个条件变量,一个条件变量的PCB等待队列中存放负责吃的线程,另一个条件变量存放负责做的线程

解法①的验证

pthread_cond_signal函数改为pthread_cond_broadcast函数
在这里插入图片描述
看一下程序的运行结果:
在这里插入图片描述
发现程序是正确的,结果也符合我们的预期,多个线程对临界资源的访问是合理的,并且也实现了互斥访问。但是这种方法尽量不要滥用,因为线程在PCB等待队列中是不会占用CPU的资源的,若每次都使用broadcast来唤醒所有的线程,那么对CPU的资源的消耗也就进一步提升了。

解法②的验证

使用两个条件变量
在这里插入图片描述
看一下程序运行的结果:
在这里插入图片描述
这个结果也是符合我们预期的,但是这两种方法,我们经常使用的是第二种解决办法,因为这种方法逻辑清晰,易于理解。

真正没有问题的代码如下:

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>



#define PTHREADNUM 2

int bowl = 1;

pthread_mutex_t g_lock;
pthread_cond_t g_condMake;
pthread_cond_t g_condEat;

//只负责吃的线程
void* MyPthreadEat(void* arg)
{
    while(1)
    {
        pthread_mutex_lock(&g_lock);
        //如果当碗中没饭的时候,则将该线程放入到PCB等待队列中,
        //等待做饭的人做饭并将其唤醒
        while(bowl < 1)
        {
            pthread_cond_wait(&g_condEat,&g_lock);
        }
        //走到这说明碗中现在是有饭的
        --bowl;
        printf("i eat %d,i am %p\n",bowl,pthread_self());

        //在操作完之后,需要对负责做的PCB等待队列发出信号,
        //唤醒正在等待通知的做的线程
        //pthread_cond_broadcast(&g_cond);
        pthread_cond_signal(&g_condMake);
        pthread_mutex_unlock(&g_lock);
    }
    return NULL;
}

//只负责做的线程
void* MyPthreadMake(void* arg)
{
    while(1)
    {
        pthread_mutex_lock(&g_lock);
        //如果当碗中有饭的时候,则将该线程放入到PCB等待队列中,
        //等待吃饭的人吃饭并将其唤醒
        while(bowl >= 1)
        {
            pthread_cond_wait(&g_condMake,&g_lock);
        }
        //走到这说明碗中现在是没饭的
        ++bowl;
        printf("i make %d,i am %p\n",bowl,pthread_self());

        //在操作完之后,需要对负责吃的PCB等待队列发出信号,
        //唤醒正在等待通知的吃的线程
        //pthread_cond_broadcast(&g_cond);
        pthread_cond_signal(&g_condEat);
        pthread_mutex_unlock(&g_lock);
    }
    
    return NULL;
}

int main()
{
    pthread_mutex_init(&g_lock,NULL);
    pthread_cond_init(&g_condEat,NULL);
    pthread_cond_init(&g_condMake,NULL);

    pthread_t tid_A[PTHREADNUM] , tid_B[PTHREADNUM];

    for(int i = 0;i < PTHREADNUM; ++i)
    {
        int ret = pthread_create(&tid_A[i],NULL,MyPthreadEat,NULL);
        if(ret < 0)
        {
            perror("pthread_create");
            return 0;
        }
        ret = pthread_create(&tid_B[i],NULL,MyPthreadMake,NULL);
        if(ret < 0)
        {
            perror("pthread_create");
            return 0;
        }    
    }

    for(int i = 0; i < PTHREADNUM; ++i)
    {
        pthread_join(tid_A[i],NULL);
        pthread_join(tid_B[i],NULL);
    }
    pthread_mutex_destroy(&g_lock);
    pthread_cond_destroy(&g_condEat);
    pthread_cond_destroy(&g_condMake);
    return 0;
}

2. 信号量

2.1 信号量的相关概念

信号量本质上是一个PCB等待队列+计数器

计数器:对资源的计数,会影响信号量的等待接口和发送接口的(唤醒接口)的逻辑。

简单来说,假设我们指定当前计算机资源有8份,并且目前均被占用,现在若有一个线程想要使用该计算机的资源,当调用信号量的等待接口时,信号量内部会自动的对计算机的资源进行判断(实际上就是对信号量中的计数器进行判断),若是资源数小于0,则就将其放入PCB等待队列中,若是资源数大于0,则直接分配相应资源。当对该资源操作完之后,就调用信号量的发送接口,对计数器进行加1,并且通知PCB等待队列中的线程出队获取资源。这就是使用信号量对临界资源操作的大概流程。

② 信号量和条件变量的差距就是:

条件变量就是需要程序员自己把握资源的数量信号量就是会自己维护资源的数量,只需要在初始化信号量的时候,指定资源的数量

信号量既可以完成同步,也可以完成互斥

互斥:

将资源计数器的初始值设置为1,线程A若拿到信号量,则线程B一定拿不到。线程A拿到信号量,就会使信号量中计数器进行减一,当线程B拿信号量的时候,判断计数器中的值为0,就会进入到PCB的等待队列中,因此也就实现了互斥。

同步:

假设当前有一个读的线程,有一个写的线程,它们均要对一个数组进行操作,那么此时,我们就可以定义两个信号量,一个读信号量,用来表示当前数组是否可读,并且初始化计数器为0,表示目前是不可读的,因为数组中没有值;另一个信号量为表示当前数组能写多少的写信号量,并且计数器初始化为数组的长度,因为数组是空的。

那么该如何实现同步呢?当我们写的线程每往数组中写一个元素的时候,首先对计数器进行减1操作,其次再唤醒读的信号量,读的信号被唤醒之后,就会对计数器进行加1操作,然后读的线程不再阻塞,就可以正常读。

然后在读的时候,每读一个数,读信号的计数器就会减1变为0,然后读信号就阻塞掉,并且会唤醒写的信号量,对写的信号量的计数器进行加1操作。这样就实现了一个同步的特点。这里需要注意的是,当往数组中写的时候,写的下标pos = (pos + 1) % 数组的长度

我会在后面的实战代码中对该功能进行实现。

那么,这里有两个面试题:
① 当一个线程调用的发送接口之后,资源技术器对其进行加1操作,若此时加1操作之后的资源计数器的结果还是小于0,此时还需要通知PCB等待队列吗?

解答:是需要的,因为需要告知PCB等待队列中目前还是有PCB进行等待的。

② 当一个线程调用发送接口之后,资源技术器进行加1操作,若此时加1操作之后的资源计数器的结果还是大于0,那么,此时还需要通知PCB等待队列吗?

解答:不需要,若是当程序中还存在着资源,那么就不需要再去通知PCB等待队列了,因为并没有太大的意义。

2.2 信号量的接口

信号量的类型为:sem_t

信号量接口包含的头文件为:#include <semaphore.h>

2.2.1 信号量的初始化

int sem_init(sem_t *sem, int pshared, unsigned int value);

参数:

  • sem:待初始化的信号量的指针
  • pshared:用于进程间还是用于线程间。(线程间:0,进程间:非0)
  • value:初始化资源的数量(实际上就是对计数器的初始化)

2.2.2 信号量的等待接口

int sem_wait(sem_t* sem);

参数:

  • sem:待要进行等待的信号量

作用

  • 会对当前资源计数器进行减1的操作
  • 判断当前资源计数器的值是否大于0

① 若是大于0,则直接将返回。
② 若是小于0,则将线程放到PCB等待队列中,并阻塞起来。

扩展:信号量的等待接口中也有sem_timedwait,即带有超时时间的接口和sem_trywait,即非阻塞的等待接口。

2.2.3 信号量的发送接口

int sem_post(sem_t *sem);

参数:

  • sem:待要进行唤醒的信号量

作用

  • 会对当前资源计数器进行加1的操作
  • 判断当前资源计数器的值是否小于0

① 若是小于0,则通知PCB等待队列
② 若是大于0,则不会通知PCB等待队列

2.2.4 信号量的销毁接口

int sem_destroy(sem_t *sem);

参数:

  • sem:待要进行销毁的信号量

3. 生产者与消费者模型

3.1 相关概念

首先我们需要知道要实现一个生产者与消费者模型,只需要执行123规则即可

1代表一个线程安全的队列

  • 队列:遵循先进先出的原则
  • 线程安全:当前队列在被其他线程操作的时候,出队操作和入队操作是保证原子性的。换句话说就是在同一时刻只有一个人能操作该队列,出队和入队时互斥的。

2代表两种角色的线程

  • 消费者线程:从线程安全队列中获取元素进行处理。
  • 生产者线程:生产元素放到线程安全队列中进行处理。

3三种关系

  • 消费者与消费者互斥
  • 生产者与生产者互斥
  • 消费者和生产资互斥加同步
    ①消费者在对资源进行操作的时候,生产者不能进行访问,这是实现了互斥。
    ②只有当生产者往线程安全的队列中生产元素的时候,消费者才可以从线程安全的队列中获取元素进行处理,这是实现了同步。

生产者与消费者的作用是:

① 支持忙闲不均,可以提高程序的运行效率。
② 队列中提供了一个缓冲区的作用,可以缓冲待要处理的元素。

3.2 使用条件变量和互斥锁实现生产者和消费者模型

首先要实现生产者与消费者模型,执行123规则即可

因此,我们应该首先定义一个类来实现一个线程安全的队列,为了保证线程安全,我们应在成员变量中给出一个互斥锁变量。为了保证生产者和消费者对资源访问的合理性,我们定义两个条件变量,用来实现同步。

两种角色的线程我们在主线程中进行初始化。

三种关系我们使用一个互斥锁和两个条件变量即可实现。

并且,我们还可以规定,Push操作为生产者线程调用的接口,Pop操作为消费者线程调用的接口。

代码如下:

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

using namespace std;

#define PTHREADNUM 4

//实现一个线程安全的队列
class SafeQueue
{
    public:
        //初始化队列
        SafeQueue() : que_(),capacity_(10)
        {
            pthread_mutex_init(&lock_,NULL);
            pthread_cond_init(&prod_,NULL);
            pthread_cond_init(&cons_,NULL);

        }
        //销毁队列
        ~SafeQueue()
        {
            pthread_mutex_destroy(&lock_);
            pthread_cond_destroy(&prod_);
            pthread_cond_destroy(&cons_);
        }

        //生产者入队操作
        void Push(int val)
        {
            /*
             * 1.首先要保证互斥,在往que_队列中插入的时候要加锁
             * 2.要对资源进行判断,当que_.size() == capacity_的时候,
             * 要进入生产者的PCB队列中进行等待
             * 3.最后,当插入完成之后,要唤醒处于消费者PCB等待队列的线程
             */ 
            pthread_mutex_lock(&lock_);
            //注意一定要是while循环判断,防止产生资源不合理的情况。
            while(que_.size() == capacity_)
            {
                pthread_cond_wait(&prod_,&lock_);
            }

            que_.push(val);
            printf("i am %p,i prod %d\n",pthread_self(),val);

            pthread_cond_signal(&cons_);
            pthread_mutex_unlock(&lock_);
        }
        void Pop(int* value)
        {
            /*
             * 1.为了保证互斥,要加锁
             * 2.要对资源进行判断,当que_.empty()为真的时候,
             * 要进入消费者的等待队列中进行阻塞
             * 3.当对资源操作完成之后,要唤醒生产者PCB等待队列中的值进行生产
             */ 
            pthread_mutex_lock(&lock_);
            
            while(que_.empty())
            {
                pthread_cond_wait(&cons_,&lock_);
            }

            *value = que_.front();
            que_.pop();
            printf("i am %p,i cons %d\n",pthread_self(),*value);
            
            pthread_cond_signal(&prod_);
            pthread_mutex_unlock(&lock_);

        }
    private:
        //使用 aaa_ 而不是 _aaa来命名成员变量,是为了与内核源码区分开来,
        //这是谷歌的一种命名规范
        //在这个队列中实现对生产资源的操作
        queue<int> que_;
        //保证这个仓库的容量
        size_t capacity_;
        
        //保证互斥
        pthread_mutex_t lock_;
        //保证同步
        //1.生产者的条件变量
        pthread_cond_t prod_;
        //2.消费者的条件变量
        pthread_cond_t cons_;

};

//由于我们要对同一临界资源进行操作,因此,我们就需要对该临界资源加锁

int value = 0;
pthread_mutex_t g_lock;

//消费者线程---用来做消费
void* ConsumePthread(void* arg)
{
    SafeQueue* sq = (SafeQueue*) arg;
    while(1)
    {
        int data;
        sq->Pop(&data);
    }

    return NULL;
}

void* ProductPthread(void* arg)
{
    SafeQueue* sq = (SafeQueue*) arg;
    while(1)
    {
        pthread_mutex_lock(&g_lock);
        sq->Push(value);
        value++;
        pthread_mutex_unlock(&g_lock);
    }
    return NULL;

}

//在主线程中创建两个线程,一个为生产者线程,一个为消费者线程
int main()
{
    pthread_t tig_A[PTHREADNUM];
    pthread_t tig_B[PTHREADNUM];

    pthread_mutex_init(&g_lock,NULL);

    //由于两个线程要对同一个资源进行操作,因此需要传入同一个队列的地址
    //由于是多线程,因此我们需要在堆上开辟空间
    SafeQueue* sq = new SafeQueue();
    for(int i = 0; i < PTHREADNUM; ++i)
    {
        int ret = pthread_create(&tig_A[i],NULL,ConsumePthread,(void*)sq);
        if(ret < 0)
        {
            printf("ConsumePthread create failed\n");
            return 0;
        }
        ret = pthread_create(&tig_B[i],NULL,ProductPthread,(void*)sq);
        if(ret < 0)
        {
            printf("ProductPthread create failed\n");
            return 0;
        }
    }

    for(int i = 0; i < PTHREADNUM; ++i)
    {
        pthread_join(tig_A[i],NULL);
        pthread_join(tig_B[i],NULL);
    }

    pthread_mutex_destroy(&g_lock);

    return 0;
}

结果如下:

在这里插入图片描述
由于我们设置的最大的容量为10个,因此会生产10个数,然后会再消费10个数,可以看到结果是正确的

3.3 使用信号量实现生产者和消费者模型(信号量的实战)

同样,要实现生产者与消费者模型,执行123规则即可

和上面一样,我们需定义一个类来实现线程安全的队列,在这个类里面,我们需要定义三个信号量,一个信号量用来实现互斥,剩下的两个信号量用来实现同步。为了保持和上一小节中举的例子一致,我们将生产者线程看为是进行写的线程,消费者线程看为进行读的线程。

并且在实现线程安全队列的时候,由于是读写线程,所以我们还需要一个数组变量用来存放写的数据的值,并且要定义两个位置指针用来标识读和写的位置。

同理,我们还可以规定,Push操作为写线程调用的接口,Pop操作为读线程调用的接口。

代码如下:

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

using namespace std;

#define CAPACITY 10
#define PTHREADNUM 2

class SafeQueue
{
    public:
        SafeQueue() : iv_(CAPACITY),capacity_(CAPACITY)
        {
            //用于线程间并要实现互斥,就要将其初始化为1
            sem_init(&lock_,0,1);
            
            //读线程的刚开始时数组中没有数,因此初始化为0
            sem_init(&read_,0,0);

            //由于刚开始时所要写的数组中没有数,因此信号量的值为capacity_
            sem_init(&write_,0,capacity_);

            //读和写的位置刚开始均为0
            pos_read = 0;
            pos_write = 0;
        }

        ~SafeQueue()
        {
            sem_destroy(&lock_);
            sem_destroy(&read_);
            sem_destroy(&write_);
        }

        void Push(int val)
        {
            /*
             * 1.这里一定要注意,不能先加锁,若是先加锁,那么当拿到这把锁之后,
             * 如果此时写的资源数已经没有了,那么就会进入PCB等待队列进行等待,
             * 并且这把锁还没有释放,造成程序的卡死,也可以称为死锁。
             * 因此,一定不要先加锁。
             * 2.在pos_write位置进行写入数据,并且要更新pos的位置
             * 3.最后再对信号量进行唤醒,保证正常的运行
             */
            sem_wait(&write_);

            sem_wait(&lock_);
            //在此处对数组进行写
            iv_[pos_write] = val;
            pos_write = (pos_write + 1) % capacity_;

            printf("i write %d,i am %p\n",val,pthread_self());
            sem_post(&lock_);

            sem_post(&read_);
        }

        void Pop(int* val)
        {
            //操作和Push方法一模一样,这里就不再做过多解释
            sem_wait(&read_);
            
            sem_wait(&lock_);
            *val = iv_[pos_read];

            pos_read = (pos_read + 1) % capacity_;

            printf("i read %d,i am %p\n",*val,pthread_self());
            sem_post(&lock_);

            sem_post(&write_);
            
        }
    private:
        vector<int> iv_;
        size_t capacity_;

        //保证互斥,初始为1
        sem_t lock_;
        //保证同步
        //1.写线程的信号量
        sem_t write_;
        //2.读线程的信号量
        sem_t read_;

        //同时需要再定义两个变量,标识当前读的位置和写的位置
        int pos_read;
        int pos_write;
};

int g_val = 0;
sem_t g_lock;

void* ReadPthread(void* arg)
{
    SafeQueue* sq = (SafeQueue*) arg;

    while(1)
    {
        int data;
        sq->Pop(&data);
    }
    
    return NULL;
}

void* WritePthread(void* arg)
{
    SafeQueue* sq = (SafeQueue*) arg;

    while(1)
    {
        sem_wait(&g_lock);

        sq->Push(g_val);
        g_val++;

        sem_post(&g_lock);
    }
    return NULL;
}

int main()
{
    pthread_t tid_A[PTHREADNUM];
    pthread_t tid_B[PTHREADNUM];

    sem_init(&g_lock,0,1);

    SafeQueue* sq = new SafeQueue();

    for(int i = 0; i < PTHREADNUM;++i)
    {
        int ret = pthread_create(&tid_A[i],NULL,ReadPthread,(void*)sq);
        if(ret < 0)
        {
            printf("pthread_create failed\n");
            return 0;
        }
        ret = pthread_create(&tid_B[i],NULL,WritePthread,(void*)sq);
        if(ret < 0)
        {
            printf("pthread_create failed\n");
            return 0;
        }
    }

    for(int i = 0; i < PTHREADNUM; ++i)
    {
        pthread_join(tid_A[i],NULL);
        pthread_join(tid_B[i],NULL);
    }
    
    sem_destroy(&g_lock);
    return 0;
}

运行结果:

在这里插入图片描述
产生了和3.2小节一样的结果,因为也是正确的。

  • 12
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值