信号量
信号量的同步与互斥实现
作用:实现线程或进程之间的同步与互斥
本质:计数器+等待队列+等待与唤醒的功能接口
场景:用于实现进程/线程间的同步与互斥
信号量实现同步:通过自身的计数器进行资源计数,对临界资源访问之前先访问信号量,通过计数判断是否有资源能够访问,若不能访问( 计数<=0 没有资源),则等待,并且计数-1,(如果没有资源了,资源为0,等待并且计数-1,变成-1),当信号量计数器为负值,就说明有多少线程正在等待;若可以访问-计数>0,则计数-1,直接访问;其它线程生产资源促使条件满足后,则判断若计数>0,计数+1(表示没人等待,只把计数+1就可以了,说明此时已经有资源了),否则若计数<0,则唤醒一个等待队列上的线程,并计数+1.(<0说明有人在等待,计数+1,就说明等待的人少了一个)
信号量实现互斥:
只需要将计数维持在0/1之间就可以实现互斥
信号量与条件变量实现同步的区别:
条件变量实现同步,没有提供给用户条件判断的功能,是需要用户自己来判断的而信号量本身有计数器,资源计数,有没有资源都是通过计数器来进行资源计数的,通过计数器,我们就能知道有没有资源,能不能访问,跟信号量最大的区别就是条件变量通过自身的计数来实现条件判断。
信号量通过自身条件判断实现同步,条件变量是通过用户进行条件判断;并且条件变量使用的是外部用户的条件判断,必须是搭配互斥锁一起使用,而信号量,是通过自身的条件判断,在内部保证了操作的原子性。
信号量的接口函数:
1. 定义信号量: sem_t类型 (sem_t类型的结构体中肯定有 计数器,阻塞队列,等待与唤醒的接口)
2. 初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
信号量变量,(sem_t 结构体至少要有计数器,等待队列,保证计数安全内部有其它的锁)
pshared :标志位,决定了当前的信号量用与进程间还是线程间,0-线程间 ,!0-进程间
(因为是一个库,线程间用全局变量就可以完成,用于进程间(进程间相互独立),实际上申请了一块共享内存,在共享内存中包含了计数器 和共享队列)
信号量的初值(由用户根据资源而定)如果定义的是互斥锁的话,那么初始值就是1,因为互斥是由0和1控制的。
成功0 失败-1
4. 在访问临界资源之前,先访问信号量,判断是否访问: 计数-1
条件判断(有没有资源自给判断)+等待
int sem_wait(sem_t *sem) ;//若计数<= 0, 则-1后阻塞;否则-1后立即返回 --阻塞函数
trywait(sem_t *sem)-- 通过自身计数判断是否满足访问条件,不满足则立即报错返回 EINVAL
timedwait(sem_t *sem,const struct timespec* abs_timeout)-- 限制时长的阻塞,超时后报错返回 ETIMEDOUT
5. 促使访问条件满足 + 1, 唤醒阻塞线程/进程
int sem_post(sem_t *sem); //唤醒信号量等待队列上的线程 且计数+1
6. 销毁信号量
int sem_destroy(sem_t * sem); //销毁信号量
注意: 一个信号量只有一个等待队列,所以我们用信号量实现生产者与消费者模型的时候,需要定义两个信号量实现同步,每一个信号量需要等待在每一个不同的等待队列上。
信号量如何实现同步与互斥
信号量实现同步
厨师做饭的例子:
sem_t _sem;
void * food()
{
int i = 0;
while(1)
{
sem_wait(&_sem); // 没有资源则阻塞等待,有资源则-1访问资源
printf("%d delicious ~~\n",i++);
}
return NULL;
}
void* cook()
{
while(1)
{
printf("cook ~~\n");
sleep(1);
sem_post(&_sem); //资源+1
}
return NULL;
}
int main()
{
sem_init(&_sem,0,0); //一开始没有资源,因为还没有做饭,所以没有资源,第三个参数为0
。。。
}
信号量实现互斥
抢车票的例子:(伪代码)
int tickets = 100;
sem_t _sem;
void* buy(void* arg)
{
while(1)
{
sem_wait(&_sem); //P操作, -1加锁, 现在初始资源为0,代表加锁状态
if(tickets > 0)
{
tickets--;
sem_post(&_sem); //V操作,+1,解锁, 唤醒等待队列上的线程
}
else
{
return NULL;
}
}
return NULL;
}
int main()
{
sem_init(&_sem,0,1); //第二个参数0是线程间,因为信号量实现互斥是0/1判断是否有资源,所以第三个参数资源初始值为1
int ret = pthread_create(&buyer,NULL,buy,NULL);
ret = pthread_create(&seller,NULL,buy,NULL);
pthread_join(buyer,NULL);
pthread_join(buyer,NULL);
sem_destroy(&_sem);
}
使用信号量实现生产者与消费者模型(代码实现)
使用信号量实现一个线程安全的阻塞队列,模拟实现生产者与消费者模型
信号量实现生产者与消费者模型的思想:
我们使用数组来实现环形的阻塞队列,通过数据空间和剩余空间的大小代表消费者和生产者线程,生产者表示现在有多少剩余空间没被使用,消费者是对数据空间的计数。
使用三个信号量实现环形阻塞队列,1个信号量实现互斥,一个用于对空闲空间进行计数 , 实现入队操作的合理性,一个用于对数据进行计数 , 实现出队操作的合理性。
注意点:
- 加锁保护的是临界资源
- 在访问是否有剩余空间或者剩余数据的时候,要先判断是否有资源,再去加锁。如果加锁了,然后再去判断是否有空闲空间,若没有则阻塞,但是这里的阻塞和条件变量的阻塞不一样,不会解锁的。你阻塞了,别人就都阻塞了
- 要先解锁再去唤醒消费者或者生产者线程,因为锁只保护临界资源
1 #include <iostream>
2 #include <vector>
3 #include <pthread.h>
4 #include <semaphore.h>
5 using namespace std;
6
7 #define MAX_QUE 10
8
9
10 class RingQueue
11 {
12 //三个信号量实现环形阻塞队列
13 //1个信号量实现互斥
14 //一个用于对空闲空间进行计数 -- 实现入队操作的合理性
15 //一个用于对数据进行计数 -- 实现出队操作的合理性
16 private:
17 vector<int> _queue; //使用数组实现环形队列
18 int _capacity; // 初始化环形队列的节点数量
19 int _step_read; // 当前读位置的数组下标
20 int _step_write; // 当前写位置的数组下标
21 sem_t _sem_lock; // 用户实现互斥的锁,信号量实现互斥的锁。
22 sem_t _sem_con; //消费者等待队列,计数器对有数据的空间进行计数
23 sem_t _sem_pro; // 生产者等待队列,计数器对有空闲的空间进行计数
24 public:
25 RingQueue(int max_que = MAX_QUE)
26 :_capacity(max_que)
27 ,_queue(max_que)
28 ,_step_read(0)
29 ,_step_write(0)
30 {
31 //初始化信号量
32 sem_init(&_sem_lock, 0, 1);// 第二个参数0是用在线程间的,第三个参数的1是 条件变量实现的互斥锁不能大于1 只能0或1,初始值1>
33 sem_init(&_sem_con,0,0);
34 sem_init(&_sem_pro,0,max_que); //生产者,初始化是节点的数量,数组有多少空节点,
35 // 就可以访问多少次,生产多少数据
36
37 }
38 ~RingQueue()
39 {
40 //销毁信号量
41 //
42 sem_destroy(&_sem_lock);
43 sem_destroy(&_sem_con);
44 sem_destroy(&_sem_pro);
45 }
46
47 bool QueuePush(int &data)
48 {
49
50 //lock和_sem_lock的顺序不能变
51 //如果加锁了,然后再去判断是否有空闲空间,若没有则阻塞,
52 //但是这里的阻塞和条件变量的阻塞不一样,不会解锁的。你阻塞了,别人就都阻塞了
53 //所以是先判断是否有资源访问,能够访问了才加锁, 加锁保护的只是临界资源的访问,
54 //
55 //
56 //sem_post(&_lock)
57 //信号量实现互斥:保证计数不大于1
```58 //对资源访问之前先sem_wait(&lock),判断计数是否>0,大于0,才会访问,并计数-1;,否则会阻塞
59 //通过0和1的计数实现互斥,访问的时候-1,计数变为0,别人就不能访问,而阻塞
60 //我访问完了之后就得解锁,
61 //就是再把计数器+1,唤醒别的线程也能访问,这就是sem_post
62
63 //数据入队
64 //1. 判断是否能够访问临界资源。判断空闲空间计数是否大于0,判断是否有空闲空间
65 sem_wait(&_sem_pro);//如果小于0.生产者等待在它的等待队列桑
66 //2. 先加锁
67 sem_wait(&_sem_lock);// lock的计数大大于1,当前若可以访问则-1
68
69 //3. 数据的入队操作
70 _queue[_step_write] = data;
71 _step_write = (_step_write + 1)% _capacity;
72 //4. 解锁
73 sem_post(&_sem_lock); // lock计数+1 此时lock为1,其他线程 可以访问互斥锁
74 //5. 数据资源的空间计数+1, 唤醒消费者
75 sem_post(&_sem_con);
76
77
78 return true;
79 }
80 bool QueuePop(int & data)
81 {
82 sem_wait(&_sem_pro); // 看是否有数据
83 sem_wait(&_sem_lock);// 有数据则加锁保护访问数据的过程
84
85 data = _queue[_step_read]; // 获取数据
86 _step_read = (_step_read+1)%_capacity;
87
88 sem_post(&_sem_lock);// 解锁操作
89 sem_post(&_sem_pro); //取出数据,并且对空闲计数+1,唤醒消费者
90
91 return true;
92 }
93 };
94