【Linux】多线程6——POSIX信号量,环形队列cp问题

掘根 2024-08-12 14:07:02 阅读 96

1.POSIX信号量

1.1.复习信号量

还记得我们在进程间通信那里讲的systemV 信号量吗!可以去这里看看哦:http://t.csdnimg.cn/JbBpE

信号量(semaphore)是一种特殊的工具,主要用于实现 同步和互斥

         在并发编程中,多个线程或进程可能会尝试同时访问同一资源,这可能导致数据的不一致或冲突。比如说两个进程访问一个共享内存,a进程想往里面写1kb大小的数据,写到一半,b进程想来读取这个共享内存的数据了,那么如果没有别的限制,b进程就直接读走了a已经写到共享内存里面的数据,这样子就导致a要传输的数据不完整了,这个是发到的数据和收到的数据不一致的问题,这种情况在管道是不存在的,因为管道是有同步和互斥机制的

互斥:任何时刻,只允许一个执行流访问共享资源

        为了解决这个问题,我们引入了信号量(Semaphore)的概念。信号量是一个计数器,用于控制对共享资源的访问。信号量是描述临界资源数量的多少。在Linux中,信号量提供了一种机制,允许多个线程或进程安全地访问共享资源。

信号量的工作原理

信号量的工作原理基于计数器的概念。

        在创建信号量时,我们需要为其设置一个初始值(这个可以自己设置),该值表示可以同时访问共享资源的线程或进程数量。每当一个线程或进程需要访问共享资源时,它会尝试获取信号量。

如果信号量的值大于0,表示还有可用的资源,线程或进程可以继续执行并访问资源。同时,信号量的值会减1。如果信号量的值为0,表示所有资源都被占用,线程或进程需要等待,直到其他线程或进程释放资源。

        当线程或进程完成对共享资源的访问后,它会释放信号量,即将信号量的值加1。这表示又有可用的资源,等待的线程或进程可以继续执行。

         每个进程想访问共享资源的一部分的时候,不是直接访问,而是先申请信号量,就像看电影的要先买票一样 

把整个临界资源视作一部分的时候,信号量初始值为1,这个信号量也叫二元信号量(因为它只有0和1两种状态)把整个临界资源视作2部分的时候,信号量初始值为2;把……

理解互斥

我们把临界资源的信号量设置为1,这样子只有1个进程能访问到这个临界资源

我们现在说要访问临界资源,得先申请信号量,信号量也是临界资源,信号量保护我们的临界资源,那么我们的信号量谁来保护呢?

我们不能直接对信号量直接加减,因为确实是有点不安全,这个我们多线程来讲,为了安全,操作系统提供了PV操作,P操作就是申请信号量,而V操作就是释放信号量,这个就更安全了!!!这两个操作也是原子的,意思就是只有两种状态(做完了或者没做),中间做了什么不知道,中间的状态也不知道,这样子就安全了!!!

原子的:只知道最初的状态和最后的状态,中间的状态不知道

1.2.POSIX信号量

好了,复习了system信号量,现在来看看POSIX信号量

POSIX和System V都是可移植的操作系统接口标准,它们都定义了操作系统应该为应用程序提供的接口标准。

POSIX信号量和System V信号量作用相同,都是用于同步和互斥操作,以达到无冲突的访问共享资源目的。System V版本的信号量只适用于实现进程间的通信,而POSIX版本的信号量主要用于实现线程之间的通信。

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

因为POSIX信号量主要用于实现线程间的同步,所以我们把它放到了多线程这里来讲。

注意: 信号量的本质是计数器,但不意味着只有计数器,信号量还包括一个等待队列。

信号量的本质上为:计数器 + PCB等待队列 + 一堆接口(等待接口,唤醒接口)

        我们来看看POSIX信号量的结构 

计数器:本质上是对资源的计数

当执行流获取信号量成功之后,信号量当中的计数器会进行减1操作,当获取失败后,该执行流就会被放到该信号量的PCB等待队列中去。当执行流释放信号量成功之后,信号量当中的计数器会进行加1操作。

又称为基于内存的信号量, 由于其没有名字, 没法通过open操作直接找到对应的信号量, 所以很难直接用于没有关联的两个进程之间。 

 1.3.POSIX信号量工作过程——PV操作

        每个执行流在进入临界区之前都应该先申请信号量,申请成功就有了操作特点的临界资源的权限,当操作完毕后就应该释放信号量。

注意: 信号量的本质是计数器,但不意味着只有计数器,信号量还包括一个等待队列。我们来看看POSIX信号量的结构 

信号量的操作包括等待(wait)和发送(post)。

等待操作(wait):执行流在申请资源时,如果信号量的值大于0,表示资源可用,进程或线程可以继续执行,并将信号量的值减1。如果信号量的值为0,表示资源不可用,这时申请信号量的进程或线程的task_struct会被放入该信号量的阻塞队列里面,直到信号量的值大于0。(P操作)发送操作(post):释放资源时,将信号量的值加1,并唤醒等待该信号量的阻塞队列里面的进程或线程。这样,其他进程或线程就可以继续执行。(V操作)

也就是说: 

信号量的PV操作:

P操作:我们将申请信号量称为P操作,申请信号量的本质就是申请获得临界资源中某块资源的使用权限,当申请成功时临界资源中资源的数目应该减一,因此P操作的本质就是让计数器减一。V操作:我们将释放信号量称为V操作,释放信号量的本质就是归还临界资源中某块资源的使用权限,当释放成功时临界资源中资源的数目就应该加一,因此V操作的本质就是让计数器加一。

PV操作必须是原子操作

        多个执行流为了访问临界资源会竞争式的申请信号量,因此信号量是会被多个执行流同时访问的,也就是说信号量本质也是临界资源。

        但信号量本质就是用于保护临界资源的,我们不可能再用信号量去保护信号量,所以信号量的PV操作必须是原子操作。

        注意: 内存当中变量的++、--操作并不是原子操作,因此信号量不可能只是简单的对一个全局变量进行++、--操作。

申请信号量失败被挂起等待

当执行流在申请信号量时,可能此时信号量的值为0,也就是说信号量描述的临界资源已经全部被申请了,此时该执行流就应该在该信号量的等待队列当中进行等待,直到有信号量被释放时再被唤醒。

什么时候会用到信号量?

多线程需要共享资源共享资源可以被局部性访问需要对共享资源的数量做计数统计,控制对共享资源的访问。、

1.4.POSIX信号量分类

POSIX信号量是一个sem_t类型的变量,但POSIX有两种信号量的实现机制:无名信号量和命名信号量。

无名信号量

        POSIX标准只是规定,无名信号量必须被放置在一段可以被多线程或者多进程所共享的内存区域中。

无名信号量有一个共享属性,分为线程共享属性和进程共享属性两类。

一个线程共享属性的无名信号量必须被放置在一个可以被进程中所有线程所共享的内存区域中,比如全局变量。一个进程共享属性的无名信号量必须被放置在共享内存区域,System V通过shmget接口实现共享内存,POSIX通过shm_open接口来实现共享内存区域。

无名信号量在使用之前必须通过sem_init接口进行初始化,之后可以通过sem_wait和sem_post接口进行操作,当不再使用无名信号量,或者放置无名信号的区域被释放之前,用户都应该通过sem_destroy接口来释放无名信号量。

 有名信号量

    有名信号量通过一个名字来作为标识,名字的格式为"/somename",这个名字的长度为MAX_NAME - 4(NAME_MAX是一个宏定义),信号量名字以'/'为开始,以'\0'字符为结尾,并且中间字符串中不能再有'/'。

    两个进程之间可以通过sem_open函数来操作同一个有名信号量,用户可以通过sem_open函数接口创建一个新的有名信号量或者打开一个已有的有名信号量。有名信号量被打开之后,进程或线程(有名信号量一般应用在进程之间的同步控制)可以通过sem_post或者sem_wait接口进行信号量操作。当一个进程不在使用有名信号量后,可以通过sem_close接口来关闭它,当系统中所有的进程都不在使用某个有名信号量后,可以通过sem_unlink接口将它从系统中移除。

 总结一下

无名信号量只可以在共享内存的情况下,比如实现进程中各个线程之间的互斥和同步,因此无名信号量也被称作基于内存的信号量;命名信号量通常用于不共享内存的情况下,比如进程间通信。

同时,在创建信号量时,根据信号量取值的不同,POSIX信号量还可以分为:

二值信号量:信号量的值只有0和1,这和互斥量很类似,若资源被锁住,信号量的值为0,若资源可用,则信号量的值为1;

计数信号量:信号量的值在0到一个大于1的限制值之间,该计数表示可用的资源的个数。

无名信号量和命名信号量的区别 

有名信号量和无名信号量的差异在于创建和销毁的形式上,但是其他工作一样。

无名信号量只能存在于内存中,要求使用信号量的进程必须能访问信号量所在的这一块内存,所以无名信号量只能应用在同一进程内的线程之间(共享进程的内存),或者不同进程中已经映射相同内存内容到它们的地址空间中的线程(即信号量所在内存被通信的进程共享)。意思是说无名信号量只能通过共享内存访问。相反,有名信号量可以通过名字访问,因此可以被任何知道它们名字的进程中的线程使用。

也就是说

单个进程中使用 POSIX 信号量时,无名信号量更简单(我们本文就讲这个)。 多个进程间使用 POSIX 信号量时,有名信号量更简单。

2.POSIX无名信号量接口

其实有名信号量和无名信号量的接口差不多,只是创建和销毁有一点差别。

我们上面说过, 单个进程中使用 POSIX 信号量时,无名信号量更简单,接下来我们就将学习无名信号的接口。

我们得先创建我们的信号量——信号量载体的类型是<code>sem_t,我们可以根据这个类型自己定义信号量对象:

#include<iostream>>

#include<semaphore.h>

using namespace std;

int main()

{

sem_t sem1;//信号量

}

2.1.初始化信号量

初始化信号量的函数叫做sem_init,该函数的函数原型如下:

函数sem_init的作用是在参数sem指向的地址上初始化一个无名信号量,其value值由参数value指定。 

参数说明:

sem:需要初始化的信号量。pshared:传入0值表示线程间共享,传入非零值表示进程间共享。value:信号量的初始值(计数器的初始值)。

返回值说明:

初始化信号量成功返回0,失败返回-1。

特别注意:参数2pshared指定此无名信号量实在进程之间被共享使用,还是在线程之间被共享使用。

如果参数pshared等于0,那么此信号量只能在一个进程内的线程之间共享使用,所以,它应该被实现在可以被所有线程都能访问到的地址上,如全局变量或者动态分配在堆上。如果参数pshared是一个非0值,那么此信号量被多个进程共享使用,它应该被实现在共享内存里,或者父子进程之间使用。

   注意:如果使用此函数去初始化一个已经被初始化的信号量,此函数的行为是未被定义的。 

2.2.销毁信号量

销毁信号量的函数叫做sem_destroy,该函数的函数原型如下:

函数sem_destroy的作用是销毁由参数sem指向的无名信号量。

        只有用sem_init初始化的无名信号量才可以用此函数来销毁,并且程序开发人员也应该使用此函数去销毁这个无名信号量。 

参数说明:

sem:需要销毁的信号量。

返回值说明:

销毁信号量成功返回0,失败返回-1。

2.3.等待信号量(申请信号量)

这个就是我们说的P操作

等待信号量的函数叫做sem_wait,该函数的函数原型如下:

参数说明:

sem:需要等待的信号量。

返回值说明:

等待信号量成功返回0,信号量的值减一。等待信号量失败返回-1,信号量的值保持不变。

其实wait函数不只一个

sem_wait系列操作,即我们所说的PV操作中的P操作,目的是锁住一个信号量。

    (1) sem_wait函数将由参数sem指定的信号量减一,即上锁操作。如果信号量的值大于0,那么执行减一操作,并且函数立即返回。如果信号量当前的值为0,那么调用进程会被加入这个信号量的阻塞队列,一直阻塞直到信号量变成大于0(由其它进程执行了sem_post操作)或者被信号中断此调用。

    (2)sem_trywait函数同sem_wait函数的作用一样,不同是如果不能立即执行加一操作,则调用进程不会堵塞而是返回一个错误,errno会被设置成EAGAIN。

    (3)sem_timedwait函数同sem_wait函数的作用一样,不同是如果不能立即执行加一操作,则调用进程会堵塞一定的时间段,这个时间段由函数参数abs_timeout指定。如果在指定的时间内信号量仍不能被锁住,则函数返回超时错误,errno会被设置成ETIMEDOUT。如果信号量的减一操作可以被立即执行,则此函数永远都不会返回超时错误,并且参数abs_timeout的有效性也不会被检查。

2.4.发布信号量(释放信号量)

与sem_wait相对应的函数就是sem_post,即我们PV操作里面的V操作。

        此函数将sem指向的信号量解锁(加一操作),加一操作后如果信号量的值变成大于0,那么另外一个因为调用sem_wait函数而被堵塞的进程或者线程将会被唤醒并且去执行对信号量的加锁操作。

发布信号量的函数叫做sem_post,该函数的函数原型如下:

参数说明:

sem:需要发布的信号量。

返回值说明:

发布信号量成功返回0,信号量的值加一。发布信号量失败返回-1,信号量的值保持不变。

3.二元信号量模拟实现互斥功能

信号量本质是一个计数器,如果将信号量的初始值设置为1,那么此时该信号量叫做二元信号量。

信号量的初始值为1,说明信号量所描述的临界资源只有一份,此时信号量的作用基本等价于互斥锁。

例如,下面我们实现一个多线程抢票系统,其中我们用二元信号量模拟实现多线程互斥。

        我们在主线程当中创建四个新线程,让这四个新线程执行抢票逻辑,并且每次抢完票后打印输出此时剩余的票数,其中我们用全局变量tickets记录当前剩余的票数,此时tickets是会被多个执行流同时访问的临界资源,在下面的代码中我们并没有对tickets进行任何保护操作。

<code>#include <iostream>

#include <string>

#include <unistd.h>

#include <pthread.h>

int tickets = 2000;

void* TicketGrabbing(void* arg)

{

std::string name = (char*)arg;

while (true){

if (tickets > 0){

usleep(1000);

std::cout << name << " get a ticket, tickets left: " << --tickets << std::endl;

}

else{

break;

}

}

std::cout << name << " quit..." << std::endl;

pthread_exit((void*)0);

}

int main()

{

pthread_t tid1, tid2, tid3, tid4;

pthread_create(&tid1, nullptr, TicketGrabbing, (void*)"thread 1");

pthread_create(&tid2, nullptr, TicketGrabbing, (void*)"thread 2");

pthread_create(&tid3, nullptr, TicketGrabbing, (void*)"thread 3");

pthread_create(&tid4, nullptr, TicketGrabbing, (void*)"thread 4");

pthread_join(tid1, nullptr);

pthread_join(tid2, nullptr);

pthread_join(tid3, nullptr);

pthread_join(tid4, nullptr);

return 0;

}

运行代码后可以看到,线程打印输出剩余票数时出现了票数剩余为负数的情况,这是不符合我们预期的。

下面我们在抢票逻辑当中加入二元信号量,让每个线程在访问全局变量tickets之前先申请信号量,访问完毕后再释放信号量,此时二元信号量就达到了互斥的效果。

<code>#include <iostream>

#include <string>

#include <unistd.h>

#include <pthread.h>

#include <semaphore.h>

class Sem{

public:

Sem(int num)

{

sem_init(&_sem, 0, num);//创建1个Sem对象的时候就已经把信号量初始化好了

}

~Sem()

{

sem_destroy(&_sem);//销毁一个Sem对象的时候顺便把信号量销毁了

}

void P()//申请信号量

{

sem_wait(&_sem);//P操作

}

void V()//发布信号量

{

sem_post(&_sem);//V操作

}

private:

sem_t _sem;//创建1个信号量

};

Sem sem(1); //二元信号量,创建sem对象的时候就已经把信号量初始化好了

int tickets = 2000;

void* TicketGrabbing(void* arg)

{

std::string name = (char*)arg;

while (true){

sem.P();//当前线程申请信号量,如果申请成功就往下走,不然就得待在信号量的阻塞队列里

if (tickets > 0){

usleep(1000);

std::cout << name << " get a ticket, tickets left: " << --tickets << std::endl;

sem.V();//这个线程抢票完毕,发布信号量,可以让别人去抢了

}

else{

sem.V();//抢票失败,发布信号量,可以让别人去抢了

break;

}

}

std::cout << name << " quit..." << std::endl;

pthread_exit((void*)0);

}

int main()

{

pthread_t tid1, tid2, tid3, tid4;

pthread_create(&tid1, nullptr, TicketGrabbing, (void*)"thread 1");

pthread_create(&tid2, nullptr, TicketGrabbing, (void*)"thread 2");

pthread_create(&tid3, nullptr, TicketGrabbing, (void*)"thread 3");

pthread_create(&tid4, nullptr, TicketGrabbing, (void*)"thread 4");

pthread_join(tid1, nullptr);

pthread_join(tid2, nullptr);

pthread_join(tid3, nullptr);

pthread_join(tid4, nullptr);

return 0;

}

运行代码后就不会出现剩余票数为负的情况了,因为此时同一时刻只会有一个执行流对全局变量tickets进行访问,不会出现数据不一致的问题。

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

        环形队列之前我们就了解过了,只要是环形队列,就存在判空判满的问题。实际上并不是真正的环形队列,而是通过数组模拟的,当数据加入到最后的位置时直接模等于数组的大小即可。通常情况下,判空判满的问题我们是通过空出一个位置,当两个指针指向同一个位置的时候是空,当只剩一个位置的时候就是满,但是我们这里不需要关注。

        总结一下,环形队列采用数组模拟,用模运算来模拟环状特性, 但是环形结构起始状态和结束状态都是一样的,不好判断为空或者为满, 解决办法:预留一个空的位置

3.1.空间资源和数据资源

在阻塞队列中,我们将队列作为整体使用,生产者和消费者在同一时刻只能有一个人进行访问,但是在环形队列里面我们可以发现,生产者和消费者关心的内容是不一样的!

生产者关注的是空间资源,消费者关注的是数据资源

对于生产者和消费者来说,它们关注的资源是不同的:

生产者关注的是环形队列当中是否有空间(blank),只要有空间生产者就可以进行生产。消费者关注的是环形队列当中是否有数据(data),只要有数据消费者就可以进行消费。

blank_sem和data_sem的初始值设置

现在我们用POSIX信号量来描述环形队列当中的空间资源(blank_sem)和数据资源(data_sem)分配情况,在我们初始信号量时给它们设置的初始值是不同的:

blank_sem的初始值我们应该设置为环形队列的容量,因为刚开始时环形队列当中全是空间。data_sem的初始值我们应该设置为0,代表环形队列里面有多少数据,因为刚开始时环形队列当中没有数据。

3.2.生产者和消费者申请和释放资源

生产者申请空间资源,释放数据资源

对于生产者来说,生产者每次生产数据前都需要先申请blank_sem:

如果blank_sem的值不为0,则信号量申请成功,此时生产者可以进行生产操作。如果blank_sem的值为0,则信号量申请失败,此时生产者需要在blank_sem的等待队列下进行阻塞等待,直到环形队列当中有新的空间后再被唤醒。

当生产者生产完数据后,应该释放data_sem

虽然生产者在进行生产前是对blank_sem进行的P操作,但是当生产者生产完数据,应该对data_sem进行V操作而不是blank_sem。生产者在生产数据前申请到的是blank位置,当生产者生产完数据后,该位置当中存储的是生产者生产的数据,在该数据被消费者消费之前,该位置不再是blank位置,而应该是data位置。当生产者生产完数据后,意味着环形队列当中多了一个data位置,因此我们应该对data_sem进行V操作。

消费者申请数据资源,释放空间资源

对于消费者来说,消费者每次消费数据前都需要先申请data_sem

如果data_sem的值不为0,则信号量申请成功,此时消费者可以进行消费操作。如果data_sem的值为0,则信号量申请失败,此时消费者需要在data_sem的等待队列下进行阻塞等待,直到环形队列当中有新的数据后再被唤醒。

当消费者消费完数据后,应该释放blank_sem:

虽然消费者在进行消费前是对data_sem进行的P操作,但是当消费者消费完数据,应该对blank_sem进行V操作而不是data_sem。消费者在消费数据前申请到的是data位置,当消费者消费完数据后,该位置当中的数据已经被消费过了,再次被消费就没有意义了,为了让生产者后续可以在该位置生产新的数据,我们应该将该位置算作blank位置,而不是data位置。当消费者消费完数据后,意味着环形队列当中多了一个blank位置,因此我们应该对blank_sem进行V操作。

3.3.必须遵守的两个规则

在基于环形队列的生产者和消费者模型当中,生产者和消费者必须遵守如下两个规则。

第一个规则:生产者和消费者不能对同一个位置进行访问。

生产者和消费者在访问环形队列时:

如果生产者和消费者访问的是环形队列当中的同一个位置,那么此时生产者和消费者就相当于同时对这一块临界资源进行了访问,这当然是不允许的。而如果生产者和消费者访问的是环形队列当中的不同位置,那么此时生产者和消费者是可以同时进行生产和消费的,此时不会出现数据不一致等问题。

如下图:

第二个规则:无论是生产者还是消费者,都不应该将对方套一个圈以上。

生产者从消费者的位置开始一直按顺时针方向进行生产,如果生产者生产的速度比消费者消费的速度快,那么当生产者绕着消费者生产了一圈数据后再次遇到消费者,此时生产者就不应该再继续生产了,因为再生产就会覆盖还未被消费者消费的数据。同理,消费者从生产者的位置开始一直按顺时针方向进行消费,如果消费者消费的速度比生产者生产的速度快,那么当消费者绕着生产者消费了一圈数据后再次遇到生产者,此时消费者就不应该再继续消费了,因为再消费就会消费到缓冲区中保存的废弃数据。

如下图:

3.4.代码实现

其中的RingQueue就是生产者消费者模型当中的交易场所,我们可以用C++STL库当中的vector进行实现。

<code>#pragma once

#include <iostream>

#include <unistd.h>

#include <pthread.h>

#include <semaphore.h>

#include <vector>

#define NUM 8

template<class T>

class RingQueue

{

private:

//P操作

void P(sem_t& s)

{

sem_wait(&s);

}

//V操作

void V(sem_t& s)

{

sem_post(&s);

}

public:

RingQueue(int cap = NUM)

: _cap(cap), _p_pos(0), _c_pos(0)

{

_q.resize(_cap);

sem_init(&_blank_sem, 0, _cap); //blank_sem初始值设置为环形队列的容量

sem_init(&_data_sem, 0, 0); //data_sem初始值设置为0

}

~RingQueue()

{

sem_destroy(&_blank_sem);

sem_destroy(&_data_sem);

}

//向环形队列插入数据(生产者调用)

void Push(const T& data)

{

P(_blank_sem); //生产者关注空间资源

_q[_p_pos] = data;

V(_data_sem); //生产

//更新下一次生产的位置

_p_pos++;

_p_pos %= _cap;

}

//从环形队列获取数据(消费者调用)

void Pop(T& data)

{

P(_data_sem); //消费者关注数据资源

data = _q[_c_pos];

V(_blank_sem);

//更新下一次消费的位置

_c_pos++;

_c_pos %= _cap;

}

private:

std::vector<T> _q; //环形队列

int _cap; //环形队列的容量上限

int _p_pos; //生产位置

int _c_pos; //消费位置

sem_t _blank_sem; //描述空间资源

sem_t _data_sem; //描述数据资源

};

相关说明:

当不设置环形队列的大小时,我们默认将环形队列的容量上限设置为8。代码中的RingQueue是用vector实现的,生产者每次生产的数据放到vector下标为p_pos的位置,消费者每次消费的数据来源于vector下标为c_pos的位置。生产者每次生产数据后p_pos都会进行++,标记下一次生产数据的存放位置,++后的下标会与环形队列的容量进行取模运算,实现“环形”的效果。消费者每次消费数据后c_pos都会进行++,标记下一次消费数据的来源位置,++后的下标会与环形队列的容量进行取模运算,实现“环形”的效果。p_pos只会由生产者线程进行更新,c_pos只会由消费者线程进行更新,对这两个变量访问时不需要进行保护,因此代码中将p_pos和c_pos的更新放到了V操作之后,就是为了尽量减少临界区的代码。

为了方便理解,我们这里实现单生产者、单消费者的生产者消费者模型。于是在主函数我们就只需要创建一个生产者线程和一个消费者线程,生产者线程不断生产数据放入环形队列,消费者线程不断从环形队列里取出数据进行消费。

#include "RingQueue.hpp"

void* Producer(void* arg)

{

RingQueue<int>* rq = (RingQueue<int>*)arg;

while (true){

sleep(1);

int data = rand() % 100 + 1;

rq->Push(data);

std::cout << "Producer: " << data << std::endl;

}

}

void* Consumer(void* arg)

{

RingQueue<int>* rq = (RingQueue<int>*)arg;

while (true){

sleep(1);

int data = 0;

rq->Pop(data);

std::cout << "Consumer: " << data << std::endl;

}

}

int main()

{

srand((unsigned int)time(nullptr));

pthread_t producer, consumer;

RingQueue<int>* rq = new RingQueue<int>;

pthread_create(&producer, nullptr, Producer, rq);

pthread_create(&consumer, nullptr, Consumer, rq);

pthread_join(producer, nullptr);

pthread_join(consumer, nullptr);

delete rq;

return 0;

}

相关说明:

环形队列要让生产者线程向队列中Push数据,让消费者线程从队列中Pop数据,因此这个环形队列必须要让这两个线程同时看到,所以我们在创建生产者线程和消费者线程时,需要将环形队列作为线程执行例程的参数进行传入。代码中生产者生产数据就是将获取到的随机数Push到环形队列,而消费者就是从环形队列Pop数据,为了便于观察,我们可以将生产者生产的数据和消费者消费的数据进行打印输出。

生产者消费者步调一致

由于代码中生产者是每隔一秒生产一个数据,而消费者是每隔一秒消费一个数据,因此运行代码后我们可以看到生产者和消费者的执行步调是一致的,也是一起成对出现的。

生产者生产的快,消费者消费的慢

我们可以让生产者不停的进行生产,而消费者每隔一秒进行消费。

<code>void* Producer(void* arg)

{

RingQueue<int>* rq = (RingQueue<int>*)arg;

while (true){

int data = rand() % 100 + 1;

rq->Push(data);

std::cout << "Producer: " << data << std::endl;

}

}

void* Consumer(void* arg)

{

RingQueue<int>* rq = (RingQueue<int>*)arg;

while (true){

sleep(1);

int data = 0;

rq->Pop(data);

std::cout << "Consumer: " << data << std::endl;

}

}

此时由于生产者生产的很快,运行代码后一瞬间生产者就将环形队列打满了,此时生产者想要再进行生产,但空间资源已经为0了,于是生产者只能在blank_sem的等待队列下进行阻塞等待,直到由消费者消费完一个数据后对blank_sem进行了V操作,生产者才会被唤醒进而继续进行生产。

但由于生产者的生产速度很快,生产者生产完一个数据后又会进行等待,因此后续生产者和消费者的步调又变成一致的了。

生产者生产的慢,消费者消费的快

当然我们也可以让生产者每隔一秒进行生产,而消费者不停的进行消费。

<code>void* Producer(void* arg)

{

RingQueue<int>* rq = (RingQueue<int>*)arg;

while (true){

sleep(1);

int data = rand() % 100 + 1;

rq->Push(data);

std::cout << "Producer: " << data << std::endl;

}

}

void* Consumer(void* arg)

{

RingQueue<int>* rq = (RingQueue<int>*)arg;

while (true){

int data = 0;

rq->Pop(data);

std::cout << "Consumer: " << data << std::endl;

}

}

虽然消费者消费的很快,但一开始环形队列当中的数据资源为0,因此消费者只能在data_sem的等待队列下进行阻塞等待,直到生产者生产完一个数据后对data_sem进行了V操作,消费者才会被唤醒进而进行消费。

但由于消费者的消费速度很快,消费者消费完一个数据后又会进行等待,因此后续生产者和消费者的步调又变成一致的了。

3.5.信号量保护环形队列的原理

在blank_sem和data_sem两个信号量的保护后,该环形队列中不可能会出现数据不一致的问题。

因为只有当生产者和消费者指向同一个位置并访问时,才会导致数据不一致的问题,而此时生产者和消费者在对环形队列进行写入或读取数据时,只有两种情况会指向同一个位置:

环形队列为空时。环形队列为满时。

但是在这两种情况下,生产者和消费者不会同时对环形队列进行访问:

当环形队列为空的时,消费者一定不能进行消费,因为此时数据资源为0。当环形队列为满的时,生产者一定不能进行生产,因为此时空间资源为0。

也就是说,当环形队列为空和满时,我们已经通过信号量保证了生产者和消费者的串行化过程。而除了这两种情况之外,生产者和消费者指向的都不是同一个位置,因此该环形队列当中不可能会出现数据不一致的问题。并且大部分情况下生产者和消费者指向并不是同一个位置,因此大部分情况下该环形队列可以让生产者和消费者并发的执行



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。