Linux--信号量
目录
1.概念
2.认识接口
3.理论加代码
3.1问题背景
3.2解决方案
3.3代码实现
1.概念
信号量是什么?
想象一下你有一个小小的计数器,这个计数器不是用来数人数或者物品数量的,而是用来控制“访问权”的。这个特殊的计数器,我们就叫它“信号量”。
信号量的作用是什么?
信号量的主要作用是帮助多个“人”(我们可以把这里的“人”想象成进程、线程或者任何需要同步的实体)在争夺同一个“资源”(比如打印机、数据库连接、共享内存等)时保持秩序,避免混乱。
之前我们知道被多个执行流同时访问的公共资源叫做临界资源,而临界资源不保护的话会造成数据不一性的问题。
们用互斥锁保护临界资源是把这个临界资源当做一个整体,只能让1个执行流访问临界资源。现在我们把临界资源分割成多个区域,当多个执行流访问不同的区域,此时不会出现数据不一性的问题了。
每个执行流先申请信号量,申请到信号量后同时访问临界资源,访问完后释放信号量。
信号量怎么工作?
信号量有两个关键操作,我们称之为“P操作”和“V操作”。
-
P操作(等待/申请):当你想要使用一个资源时,你会走到信号量面前,告诉它你想要访问那个资源。信号量会看看它的小计数器上写着多少。如果计数器大于0,说明还有资源可用,它就会把计数器减1,然后让你进去使用资源。如果计数器是0,说明资源都被别人占用了,它会让你在一边等着,直到有人释放资源。
-
V操作(释放/通知):当你用完资源后,你会再次走到信号量面前,告诉它你已经用完了。信号量就会把它的计数器加1,表示又多了一个资源。然后,如果有人在等这个资源(因为之前的P操作被挡住了),信号量就会叫醒其中一个人,让他进去使用资源。
2.认识接口
sem_init 函数是用于初始化一个未命名的信号量(semaphore)的函数。是线程间或进程间同步的一种机制。通过信号量,程序可以控制对共享资源的访问,确保在同一时间内只有一个线程(或进程)能够访问某个特定的资源。
参数
- sem:指向要初始化的信号量对象的指针。
- pshared:控制信号量的作用域。如果pshared的值非0,则信号量在进程间共享;如果为0,则信号量仅在调用进程内的线程间共享。
- value:信号量的初始值。这个值必须是非负的。
返回值
- 成功时,sem_init 返回0。
- 失败时,返回-1,并设置errno以指示错误
sem_destroy函数的作用是释放与信号量相关联的资源,确保系统资源的正确回收。
参数
- sem:指向要销毁的信号量对象的指针。
返回值
- 成功时,sem_destroy 返回 0。
- 失败时,返回 -1,并设置 errno 以指示错误。可能的错误包括 EINVAL,表示传入的信号量不是一个有效的信号量。
sem_wait函数是信号量同步机制中的关键部分,用于实现线程或进程间的同步。
该函数用于信号量的P操作,从信号量的值中减去1,如果信号量的值变为0,则当前线程将被阻塞,直到信号量的值被其他线程通过 sem_post 增加。
参数
- sem:指向要操作的信号量对象的指针。
返回值
- 成功时返回0。
- 失败时返回-1,并设置errno以指示错误。
sem_post函数用于信号量的V操作,主要作用是给信号量的值加上一个“1”,并可能唤醒一个或多个在该信号量上等待的线程。
参数
- sem:指向要操作的信号量的指针。
功能
- 增加信号量值:sem_post函数将信号量的值增加1。
- 唤醒等待线程:如果有线程因为调用sem_wait函数而在该信号量上等待(即信号量的值为0且线程被阻塞),那么sem_post函数可能会唤醒其中一个等待的线程。具体唤醒哪个线程取决于操作系统的线程调度策略。
返回值
- 成功时,sem_post返回0。
- 失败时,返回-1,并设置errno以指示错误。可能的错误包括EINVAL,表示传入的信号量不是一个有效的信号量。
3.理论加代码
3.1问题背景
如何基于环形队列实现生产消费模型:
环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留一个空的位置,作为满的状态
比如消费者(head)指向头,生产者(end)指向尾。
当head和end 指向一个位置时:
1.队列为空,让谁先访问?当然只能让生产者先生产。
2.队列满了,让谁再访问?当然只能让消费者来消费
这其实就满足了一定顺序性了(同步),而且是满足了互斥特定的
head和end指向不同的位置:
队列不为空&&队列不为满--此时生产和消费同时进行!因为head和end方位的是同一块资源的不同位置,多线程当然可以并发访问的了。
基于以上两种情况,我们就可以让线程再局部上体现互斥和同步的特点,在宏观上体现出并发了。
结论:
a.不让生产者把消费者套一个圈。
b.不能让消费者,超过生产者
以上条件,用信号量(用于做互斥与同步的)就能满足了。
3.2解决方案
对于以上问题,消费者最关心的是数据资源(取数据)。对于生产者来说,最关心的就是空间资源(放数据)。信号量就是描述资源数量的。因此我们可以定义两个信号量:
sem_t data_sem=0,初始化肯定是0(数据资源),sem_t space_sem=N(空间资源)。
- 生产者p(space_sem)申请空间资源--,生产动作v(data_sem)数据资源++,生产者申请到空间了,但数据还是在那块空间,当然v的是数据的信号量。
- 消费者p(data_sem)申请数据资源--,消费动作v(space_sem)空间资源++,消费者把数据拿走了只留下空间了,当然v的是空间的信号量。
一开始,data资源为0,也就是为空情况,消费线程和生产线程都来了,消费线程是注定要被挂起。deat资源为N,也就是为满的情况,消费线程和生产线程都来了,生产线程主动要被挂起。这就完成了互斥(理论上互斥)
data和space达到动态平衡时,消费线程和生产线程都能进来,他们的两个下标也不会指向同一个位置,这就达到了并发的效果。
因为data和space资源最大为N,这注定了生产线程不能一直无脑生产最大为N,消费者不能无脑消费,最大为N。
3.3代码实现
RingQueue.hpp:
#pragma once #include #include #include #include #include template class RingQueue { private: void P(sem_t &s) { sem_wait(&s);//对信号量-- } void V(sem_t &s) { sem_post(&s);//对信号量++ } public: RingQueue(int max_cap) : _ringqueue(max_cap), _max_cap(max_cap), _c_step(0), _p_step(0) { sem_init(&_data_sem, 0, 0); sem_init(&_space_sem, 0, max_cap); pthread_mutex_init(&_c_mutex, nullptr); pthread_mutex_init(&_p_mutex, nullptr); } void Push(const T &in) //生产者 { P(_space_sem); pthread_mutex_lock(&_p_mutex); //? _ringqueue[_p_step] = in;//生产的位置 _p_step++; _p_step %= _max_cap;//维持环状 pthread_mutex_unlock(&_p_mutex); V(_data_sem);//数据资源多了一个 } void Pop(T *out) // 消费 { P(_data_sem);//申请数据资源 pthread_mutex_lock(&_c_mutex); //? *out = _ringqueue[_c_step];//在消费者下标进行消费 _c_step++; _c_step %= _max_cap; pthread_mutex_unlock(&_c_mutex); V(_space_sem);//数据取走,空间空出来,归还空间 } ~RingQueue() { sem_destroy(&_data_sem); sem_destroy(&_space_sem); pthread_mutex_destroy(&_c_mutex); pthread_mutex_destroy(&_p_mutex); } private: std::vector _ringqueue;//vector模拟的环形队列结构 int _max_cap;//容量 int _c_step;//生产者下标 int _p_step;//消费者下标 sem_t _data_sem; // 消费者关心 sem_t _space_sem; // 生产者关心 pthread_mutex_t _c_mutex;//生产者互斥锁 pthread_mutex_t _p_mutex;//消费者互斥锁 };
细节:
1.为了完成多生产和多消费的模型,消费者和生产者的同步和互斥问题我们的消费队列已经解决了。那么我们就要维护生产者和生产者的互斥关系,还有消费者和消费者之间的互斥关系。由于环形队列的下标也是属于临界资源的,如果不维持关系内部的互斥关系,是一定会破坏环形队列结构的。所以势必要引入生产者互斥锁和消费者互斥锁。
2.先加锁好还是先申请获取信号量好呢?
先申请信号量好,所有线程先瓜分好信号量,在其它线程进行等待锁的时候,此时资源已经申请好了,这样提高了解决问题的实际效率。
3.信号量这里,对资源进行使用,申请,为什么不判断一下条件是否满足?
信号量本身就是判断条件! 信号量:是一个计数器,是资源的预订机制。预订:在外部,可以不判断资源是否满足,就可以知道内部资源的情况!
信号量原理:一元信号量(或二元信号量)主要用于实现资源的互斥访问。当一个线程(或进程)获取了信号量(将其值从1减为0),它便获得了对某个共享资源的独占访问权,其他试图获取该信号量的线程将被阻塞,直到信号量被释放(其值从0加回1)。
信号量对公共资源使用时可以整体使用,也可以不整体使用。整体使用就是把整个资源看作一份。当看整体的时候,当二元信号量的值为1时,表示资源未被占用,线程可以获取信号量并进入临界区;当信号量的值为0时,表示资源已被占用,其他线程必须等待。那么这就等同于互斥锁了。
Task.hpp:
#pragma once #include class Task { public: Task() { } Task(int x, int y) : _x(x), _y(y) { } void Excute() { _result = _x + _y; } void operator ()() { Excute(); } std::string debug() { std::string msg = std::to_string(_x) + "+" + std::to_string(_y) + "=?"; return msg; } std::string result() { std::string msg = std::to_string(_x) + "+" + std::to_string(_y) + "=" + std::to_string(_result); return msg; } private: int _x; int _y; int _result; };
main.cc:多生产者生产,多消费者消费
#include "RingQueue.hpp" #include "Task.hpp" #include #include #include #include void *Consumer(void*args) { RingQueue *rq = static_cast(args); while(true) { Task t; // 1. 消费 rq->Pop(&t); // 2. 处理数据 t(); std::cout
- sem:指向要操作的信号量的指针。
- sem:指向要操作的信号量对象的指针。
- sem:指向要销毁的信号量对象的指针。