「前言」文章是关于Linux多线程方面的知识,上一篇是 Linux多线程详解(四),今天这篇是 Linux多线程详解(五),内容大致是信号量,讲解下面开始!
「归属专栏」Linux系统编程
「主页链接」个人主页
「笔者」枫叶先生(fy)
「枫叶先生有点文青病」「每篇一句」
求其上,得其中;
求其中,得其下;
求其下,必败。
——《孙子兵法》
十、POSIX信号量
10.1 分析之前代码的不足
在进入话题正文之前先谈三个概念:串行、并行、并发
- 并发:并发是指同时处理多个任务,并且这些任务可能会相互影响,需要协调和管理。例如,在一个多用户系统中,多个用户可能同时访问同一个资源,需要通过并发控制来避免冲突和竞争
- 并行:并行是指同时执行多个任务,每个任务都在不同的处理器上执行,相互之间独立,不会相互影响。例如,在一个多处理器系统中,多个任务可以同时在不同的处理器上执行,提高系统的处理能力和效率
- 串行:串行是指按照一定的顺序依次执行任务,每个任务必须在前一个任务完成后才能开始执行。在串行执行中,每个任务都是独占CPU资源的,直到该任务执行完毕后,才会释放CPU资源给下一个任务使用
下面进入正文,代码样例是上一篇的Linux多线程(四)的代码
- 一个线程在操作临界资源的时候,一定是要满足临界资源的条件的
- 可是,是否满足条件,我们是无法直接得知的(不能事先得知,在没有访问临界资源之前,是无法得知是否满足条件)
- 只能先进行加锁,再检测,再操作,再解锁。根据检测的结果决定线程下一步怎么走
- 进行检测也是在访问临界资源
- 比如上面的代码样例中,线程是无法知道是否满足生产条件的,线程必须申请到锁,进入临界区才能检测是否满足条件,根据检测的结果决定线程下一步怎么走
注意:只要我们对临界资源进行加锁,就默认了对这个资源进行整体使用
但实际情况可能存在:一份临界资源,允许被不同线程同时访问不同的区域(把一份共享资源分成许多份使用) ,可以进行这样操作就是信号量
10.2 信号量概念
信号量主要用于线程同步和互斥的
POSIX信号量和SystemV信号量作用相同,都是用于同步和互斥操作,达到无冲突的访问共享资源目的, 但POSIX可以用于线程间同步和互斥
信号量:semaphore
信号量的本质:
信号量(信号灯)本质是一个计数器,是描述临界资源中资源数目的计数器,信号量能够更细粒度的对临界资源进行管理
信号量解释:
- 以例子进行解释,比如一个电影院,这个电影院看作是临界资源,电影院里面的每一个座位看作是临界资源划分成的一个个子资源;
- 我只有买了票,这个座位在一定时间内才是归属我的,即便我买了票不去,这个座位也是我的,反过来,你没买票,这个座位就一定不是你的。买的票一定对应着一个座位。
- 同样道理,我想用某种资源的时候,可以对这种资源进行预订,就相当于买票,买了票我就已经预订了这个座位,我预订了这个资源,它在未来的某一时间一定属于我
- 假设电影院的座位只有100个,你不能卖出第101张票吧,信号量就相当于这里的票一样,票卖完了就不允许再卖了,再卖就会发生 ‘座位冲突’,也就对应进程访问该资源会发生冲突
- 线程想要访问某个临界资源的条件是:申请到信号量,申请到了信号量就相当于预订了临界资源的某一部分小资源,就允许该线程对该资源的访问
- 将一块临界资源拆分成一个个的子资源,不同的线程可以对不同的子资源进行访问,从而实现并发(程序员编码保证不同的线程可以并发访问临界资源的不同区域)
而申请信号量的前提是:所有线程要看到一个同一个信号量,所以信号量本身就是临界资源
- 既然信号量是临界资源,是临界资源就要保证自身的安全性。
- 信号量的本质是一个计数器,计数器的操作只有 ++(递增)和 --(递减)
- 进行 ++ 和 -- 操作就要保证信号量自身的安全性,所以这个计数器就不能只是单纯整型int,因为这样的 ++ 和 -- 操作不是原子性的
- 所以Linux对信号量进行封装了一种类型 sem_t,这种类型可以保证 ++ 和 -- 操作是原子性的
只要申请到了信号量,在未来就一定可以拥有该临界资源的一部分。
申请信号量的本质:对临界资源中特定小块资源进行预订(申请信号量就是一种预订机制)
信号量的核心操作是 ++ 和 --(PV操作)
- 对信号量++,归还信号量资源(V操作)
- 对信号量--,申请信号量资源(P操作)
信号量这种预订机制,就有可能我们在真正访问临界资源之前,我们就可以提前知道临界资源的使用情况
- 只要申请信号量成功,临界资源就一定会有一部分资源是你的
- 只要申请信号量失败,就说明条件不就绪(不满足),你只能进行等待
- 所以,就不需要像互斥锁,只有加锁后进行判断才能知道临界资源的使用情况
提前知道临界资源的使用情况:类比上面的电影院(临界资源),有一场电影只有100张票(临界资源分成100个小资源),只卖了80张,每张电影票对应着一个唯一的座位,电影还没有开始我们就已经预知了该电影院资源(座位)的使用情况
10.3 POSIX信号量函数
初始化信号量
初始化信号量的函数叫做 sem_init,man 3 sem_init 查看
函数:sem_init
头文件:#include <semaphore.h>
函数原型:
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
第一个参数sem:需要初始化的信号量
第二个参数pshare:0表示线程间共享,非零表示进程间共享
第三个参数value:信号量初始值(信号量初始值)
返回值:
初始化信号量成功返回0,失败返回-1,错误码被设置
销毁信号量
销毁信号量的函数叫做 sem_destroy, man 3 sem_destroy查看
函数:sem_destroy
头文件:#include <semaphore.h>
函数原型:
int sem_destroy(sem_t *sem);
参数:
参数sem:需要销毁的信号量
返回值:
销毁信号量成功返回0,失败返回-1,错误码被设置
等待信号量(申请信号量)(P操作)
等待信号量的函数叫做 sem_wait,man 3 sem_wait查看
函数:sem_wait
头文件:#include <semaphore.h>
函数原型:
int sem_wait(sem_t *sem);
参数:
参数sem:需要等待的信号量
返回值:
等待信号量成功返回0,信号量的值-1
等待信号量失败返回-1,错误码被设置,信号量的值保持不变
发布信号量(释放信号量)(V操作)
发布信号量的函数叫做 sem_post,man 3 sem_post查看
函数:sem_post
头文件:#include <semaphore.h>
函数原型:
int sem_post(sem_t *sem);
参数:
参数sem:需要发布的信号量
返回值:
等待信号量成功返回0,信号量的值+1
等待信号量失败返回-1,错误码被设置,信号量的值保持不变
注意:信号量为1时,说明临界资源是一整个整体使用的(信号量所描述的临界资源只有一份),提供互斥功能(此时信号量的作用基本等价于互斥锁),提供互斥功能的信号量也叫二元信号量
十一、基于环形队列的生产消费模型
11.1 环形队列
环形队列本质上是一个数组,环形队列是固定大小的,在数据结构学过,不解释
环形队列的判空判满问题:(1)计数器,(2)空出最后一个位置
11.2 环形队列的生产消费模型
以但单生产单消费为例,生产者和消费者在这两种情况下可能会访问同一个位置:(1)环形队列空的时候,(2)环形队列满的时候,对于其他情况生产者和消费者访问的位置都不一样
环形队列的生产消费模型,必须要满足两个条件:
- 消费者不能超过生产者
- 无论是生产者还是消费者,不允许将对方套一个圈以上
对于生产者和消费者来说,它们关注的资源是不同的:
- 生产者关注的是环形队列当中是否有空间,只要有空间生产者就可以进行生产。
- 消费者关注的是环形队列当中是否有数据,只要有数据消费者就可以进行消费。
这里就需要用信号量来描述环形队列当中的空间资源和数据资源,需要创建两个信号量,假设是A,B,A信号量用来标识空间资源的多少,B信号量用于标识数据资源的多少,并且这两个信号量初始化的值不同:
- A信号量初始值我们应该设置为环形队列的容量,因为刚开始时环形队列当中全是空的,空间资源为环形队列的容量
- B信号量初始值我们应该设置为0,因为刚开始时环形队列当中没有数据
生产者申请空间资源,释放数据资源
- 对于生产者来说,生产者每次生产数据前都需要先申请(等待)A信号量,
- 如果A信号量的值不为0,则信号量申请成功,A信号量-1(即等待A信号量成功,P操作),此时生产者可以进行生产操作。
- 如果A信号量的值为0,则信号量申请失败,此时生产者需要在A信号量的等待队列下进行阻塞等待,直到环形队列当中有新的空间后再被唤醒
当生产者生产完数据后,应该释放B信号量,因为此时队列中的数据已经+1,B信号量也需要+1(即释放(发布)B信号量,V操作)
消费者申请数据资源,释放空间资源
- 对于消费者来说,消费者每次消费数据前都需要先申请(等待)B信号量,
- 如果B信号量的值不为0,则信号量申请成功,B信号量-1(即等待A信号量成功,P操作),此时消费者可以进行消费操作。
- 如果B信号量的值为0,则信号量申请失败,此时消费者需要在B信号量的等待队列下进行阻塞等待,直到环形队列当中有新的数据后再被唤醒
当消费者消费完数据后,应该释放A信号量,因为此时队列中的数据已经-1,空出一个空间资源,A信号量也需要+1(即释放(发布)A信号量,V操作)
下面进行编写代码
11.3 代码实现
Linux多线程(四)实现的是是基于queue的生产者消费者模型,其空间可以动态分配(采用互斥锁+条件变量实现);现在基于固定大小的环形队列重写这个生产者消费者模型(采用POSIX信号量实现)
为了方便理解,实现的代码也是单生产者,单消费者
RingQueue.hpp
用C++STL库当中的vector模拟实现环形队列
#pragma once
#include <iostream>
#include <vector>
#include <semaphore.h>
#include <cassert>
static const int gcap = 5;
template <class T>
class RingQueue
{
private:
// P操作,等待信号量
void P(sem_t &sem)
{
int n = sem_wait(&sem);
assert(n == 0);
(void)n;
}
// V操作,发布信号量
void V(sem_t &sem)
{
int n = sem_post(&sem);
assert(n == 0);
(void)n;
}
public:
RingQueue(const int &cap = gcap) : _ringQueue(cap), _cap(cap)
{
sem_init(&_spaceSem, 0, _cap);
sem_init(&_dataSem, 0, 0);
_productorStep = _consumerStep = 0;
}
// 生产者生产数据
void Push(const T &in)
{
// 申请信号量,申请成功继续执行代码,失败阻塞等待
P(_spaceSem);
_ringQueue[_productorStep] = in;
_productorStep++;
_productorStep %= _cap;
// 此时数据+1,发布_dataSem信号量
V(_dataSem);
}
// 消费者消费数据
void Pop(T *out)
{
// 申请信号量,申请成功继续执行代码,失败阻塞等待
P(_dataSem);
*out = _ringQueue[_consumerStep];
_consumerStep++;
_consumerStep %= _cap;
// 此时数据-1,发布_spaceSem信号量
V(_spaceSem);
}
~RingQueue()
{
sem_destroy(&_spaceSem);
sem_destroy(&_dataSem);
}
private:
std::vector<T> _ringQueue;
int _cap;
sem_t _spaceSem; // 空间资源
sem_t _dataSem; // 数据资源
int _productorStep; // 生产者的下标,共用_ringQueue,两套下标
int _consumerStep; // 消费者的下标,共用_ringQueue
};
Main.cc
主函数只需要创建一个生产者线程和一个消费者线程,生产者线程不断生产数据放入环形队列,消费者线程不断从环形队列里取出数据进行消费
#include "RingQueue.hpp"
#include <pthread.h>
#include <unistd.h>
#include <ctime>
using namespace std;
void *consumer(void *ringqueue)
{
RingQueue<int> *rq = static_cast<RingQueue<int> *>(ringqueue);
while (true)
{
int data;
rq->Pop(&data);
cout << "消费完成,消费的数据是:" << data << endl;
sleep(1);
}
return nullptr;
}
void *productor(void *ringqueue)
{
RingQueue<int> *rq = static_cast<RingQueue<int> *>(ringqueue);
while (true)
{
int data = rand() % 10 + 1;
rq->Push(data);
cout << "生产完成,生产的数据是:" << data << endl;
sleep(1);
}
return nullptr;
}
int main()
{
srand((unsigned int)time(nullptr) ^ 0x4589315); // 用于获取随机数
RingQueue<int> *rq = new RingQueue<int>();
// c:consumer p:productor
pthread_t c, p;
pthread_create(&c, nullptr, consumer, rq);
pthread_create(&p, nullptr, productor, rq);
// 线程等待
pthread_join(c, nullptr);
pthread_join(p, nullptr);
delete rq;
return 0;
}
生产者消费者步调一致
生产者每隔一秒生产一个数据,而消费者是每隔一秒消费一个数据,因此运行代码后我们可以看到生产者和消费者的执行步调是一致的
测试运行结果
生产者生产快,消费者消费慢
修改代码,生产者生产快,消费者消费慢
测试运行结果
- 第一条打印是因为两个线程同时执行,消费者线程消费了一次数据才sleep(1)
- 此时由于生产者生产的很快,运行代码后一瞬间生产者就将环形队列打满了,此时生产者想要再进行生产,但空间资源已经为0了,生产者只能阻塞等待空间资源
- 消费者消费一个环形队列里面的数据,生产者又会生产一个数据,因此后续生产者和消费者的步调又变成一致的了
生产者生产慢,消费者消费快
修改代码,生产者生产慢,消费者消费快
测试运行
- 第一条打印是因为两个线程同时执行,生产者线程生产了一个数据才sleep(1)
- 此时由于消费者消费的很快,运行代码由于环形队列中没有数据,只能阻塞等待生产者生产数据
- 生产者生产一个数据,消费者又会消费一个数据,因此后续生产者和消费者的步调又变成一致的了
文章来源:https://www.toymoban.com/news/detail-471622.html
--------------------- END ----------------------文章来源地址https://www.toymoban.com/news/detail-471622.html
「 作者 」 枫叶先生
「 更新 」 2023.6.3
「 声明 」 余之才疏学浅,故所撰文疏漏难免,
或有谬误或不准确之处,敬请读者批评指正。
到了这里,关于『Linux』第九讲:Linux多线程详解(五)_ 信号量的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!