多线程(11)多线程同步之信号量(Linux实现)
1. 信号量
1.1 什么是信号量?
信号量也是一种锁,线程获取不到信号量的时候进入睡眠,直至有信号量释放出来时,才会唤醒睡眠的线程,进入临界区执行。
信号量本质上是一个计数器,用于多进程/多线程对共享数据对象的读取。
1.2 信号量使用方法
在进入一个关键代码段之前,线程必须获取一个信号量;
一旦该关键代码段完成了,那么该线程必须释放信号量。
其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。
1.3 使用场景:适用于等待时间较长的临界区
由于使用信号量时,线程会睡眠,所以等待的过程不会占用CPU时间。所以信号量适用于等待时间较长的临界区
1.3.1 信号量和自旋锁的选择
信号量消耗的CPU时间的地方在于使线程睡眠和唤醒线程,
如果 (使线程睡眠 + 唤醒线程)的CPU时间 > 线程自旋等待的CPU时间,那么可以考虑使用自旋锁。
1.4 信号量的分类
在学习信号量之前,我们必须先知道——Linux提供两种信号量:
1.内核信号量,由内核控制路径使用
2.用户态进程使用的信号量,这种信号量又分为POSIX信号量和SYSTEM V信号量。
POSIX信号量,用于线程间同步
系统V信号量,用于进程间同步
1.4.1 有名信号量和无名信号量
POSIX信号量又分为有名信号量和无名信号量
有名信号量一般是用在进程间同步,无名信号量一般用在线程间同步。
有名信号量,其值保存在文件中, 所以它可以用于线程也可以用于进程间的同步。
无名信号量,其值保存在内存中。
本博客主要是讲POSIX信号量的无名信号量
区别: 主要在于两种信号量初始化和销毁的方式不同。
1.4.2 二值信号量和计数信号量
信号量有二值信号量和计数信号量2种,其中二值信号量比较常用。
二值信号量表示信号量只有2个值,即0和1。信号量为1时,表示临界区可用,信号量为0时,表示临界区不可访问。
二值信号量表面看和自旋锁很相似,区别在于争用自旋锁的线程会一直循环尝试获取自旋锁,
而争用信号量的线程在信号量为0时,会进入睡眠,信号量可用时再被唤醒。
计数信号量有个计数值,比如计数值为5,表示同时可以有5个线程访问临界区。
1.5 信号量的P,V操作
由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),
他们的行为是这样的:
P(sv): wait()操作,消费者,释放资源:如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行。
V(sv):post()操作,生产者,申请资源:如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1。
1.6 信号量(POSIX的无名信号量)有4种操作接口
-
初始化(initialize),也叫做建立(create)
int sem_init(sem_t *sem, int pshared, unsigned int value);
返回值:成功返回0,失败返回-1;
参数sem:表示指向信号结构的指针。
参数pshared:不是0 的时候该信号量在进程间共享,否则只能在当前进程的所有线程间共享。
参数value:信号量的初始值,需要的信号量数目,它的值几乎总是1。 -
等信号(wait),也可叫做挂起(suspend)-消费者,信号量的值减1
信号量减一操作,有线程申请资源
int sem_wait(sem_t *sem); //相当于P操作,即申请资源。
成功返回0,否则返回-1
参数sem:指向一个信号量的指针 -
给信号(signal)或发信号(post) -生产者,信号量的值加1
信号量加一操作,有线程释放资源
int sem_post(sem_t *sem); //相当于V操作,生产者。
成功返回0,否则返回-1
参数sem:指向一个信号量指针 -
清理(destroy)
int sem_destory(sem_t *sem);
1.7 特点:会有上下文切换/睡眠,适用于较长的临界区
线程会睡眠,所以等待的过程不会占用CPU时间。
所以信号量适用于等待时间较长的临界区。
1.8 semaphore源码解析
struct semaphore {
spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
可以发现信号量结构体中有个自旋锁,这个自旋锁的作用是保证信号量的down和up等操作不会被中断处理程序打断。
2. 代码例子- POSIX 无名信号量
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#define MAXSIZE 10
int stack[MAXSIZE];
int size =0;
sem_t sem;
void privide_data(void)
{
int i;
for(i =0;i<MAXSIZE;++i)
{
stack[i] = i;
sem_post(&sem);
}
}
void handle_data(void)
{
int i;
while((i = size ++) <MAXSIZE)
{
sem_wait(&sem);
printf("cross : %d X %d = %d \n",stack[i],stack[i],stack[i] * stack[i]);
sleep(1);
}
}
int main()
{
pthread_t privider,handler;
sem_init(&sem,0,0);
pthread_create(&privider,NULL,(void *)&privide_data,NULL);
pthread_create(&handler,NULL,(void *)&handle_data,NULL);
pthread_join(privider,NULL);
pthread_join(handler,NULL);
sem_destroy(&sem);
return 0;
}
结果:
gcc sem.c -pthread
[root@localhost semTest]# ./a.out
cross : 0 X 0 = 0
cross : 1 X 1 = 1
cross : 2 X 2 = 4
cross : 3 X 3 = 9
cross : 4 X 4 = 16
cross : 5 X 5 = 25
cross : 6 X 6 = 36
cross : 7 X 7 = 49
cross : 8 X 8 = 64
cross : 9 X 9 = 81
[root@localhost semTest]#
3. 信号量和互斥锁的区别:
3.1 相同点:(会有上下文切换/睡眠)
信号量和互斥锁都会在获取锁失败情况下睡眠,会有上下文切换,线程会释放cpu给其他线程。
这点和自旋锁的区别。
3.2 不同点:(流程和锁住)
3.2.1 信号量不一定是锁定某一个资源,而是流程上的概念
信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作,这是是流程上的概念。
信号量不一定是锁定某一个资源,而是流程上的概念。
比如:有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类
3.2.2 互斥量则纯粹是“锁住某一资源”的概念
互斥锁是用在多线程多任务互斥,在锁定期间内,其他线程无法对被保护的数据进行操作。
3.2.3 作用域
信号量: 进程间或线程间(linux仅线程间)
互斥锁: 线程间
3.3 使用场景:
3.3.1 在有些情况下两者可以互换。
3.3.2 独占情况下使用互斥量
因为mutex较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。
参考
https://www.cnblogs.com/han-bing/p/6166391.html
https://blog.csdn.net/wangcg123/article/details/79666424
https://blog.csdn.net/qq_41248872/article/details/82991949