在研究synchronized原理的时候,重量锁会使用操作系统的Mutex机制,而且一些文章会提到信号量(Semaphore)这个东西。所以现在对这些概念简单的了解一下。
信号量(Semaphore)
信号量概述
信号量主要是用来保护共享资源的,控制对共享资源的并发访问。当线程或进程获得信号量的时候,可以访问共享资源,操作完毕后释放信号量。当线程或进程获取不到信号量的时候,会将自身加入到一个等待队列里面去睡眠,直到其他线程释放信号量,处于等待队列的线程或进程会被唤醒,当被唤醒之后,立即重新从睡眠的地方开始执行,再次去获取信号量,获取到后访问共享资源。
信号量分类
在Linux中提供了两种信号量:
- 内核信号量
- 用户态进程使用的信号量,又分为POSIX信号量和SYSTEM V信号量
这里不谈每种信号量的具体结构和源码分析,只介绍其中的原理,和这种原理到底能够为我们开发程序带来什么好处,我们能够用这种解决什么问题。
为什么会有信号量
信号量的出现视为了解决多线程或多进程访问共享资源时所带来的并发问题,需要通过某种方式来协调这种访问,这种协调方式就是信号量。那么信号量到底是如何工作的呢?
信号量工作原理
信号量在本质上是一种计数器。当一个线程要访问共享资源的时候,需要先读取信号量的值来判断资源是否可用:
信号量 > 0:表示资源可用请求,对信号量-1。
信号量 = 0:无资源可用,进程会进入睡眠状态,放入到等待队列直至资源可用。
当这个线程访问共享资源完毕之后,会将信号量+1
对信号量的值进行减少和增加操作均为原子操作,因为这个信号量是被多个线程或进程访问和修改的。
对信号量的值进行减少的操作称为P操作(荷兰语proberen,测试),又称wait()操作;进行增加的操作称为V操作(荷兰语verhogen,增加),又称signal()操作。
以上就是信号量如何协调多线程之间同步,其实也挺简单的。
举个例子简单了解一下,有一个停车场,有10个停车位,也就是信号量为10,停车场外有11辆车,第一辆车通过收费口进入到停车场,后面的车同样进入,当第十一辆车进入到停车场的时候,收费口会发现没有停车位了,就会让这第十一辆车等待,直到前面十辆车中有一个车开出停车场,这个第十一辆车才能进入。
二进制信号量
当这个信号量的值不受限制的时候,称为计数信号量
当这个信号量的值为0和1的时候,称为二进制信号量,又称为互斥锁(mutex)
二进制信号量是最简单的一种锁,它只用两种状态(占有与非占用)。它适合只能被唯一一个线程访问的资源。当二元信号量处于非占用状态时,第一个试图获取该二元信号量的线程会获得该锁,并将二元信号量置为占用状态,伺候其他的所有试图获取该二元信号量的线程将会等待,直到该锁被释放。
其实严格意义上来说不能成为锁,只不过是通过这种方式实现了锁的效果。
互斥(Mutex)
互斥锁(mutual exclusive, mutex)将这段代码给锁住,同一时刻只能有一个线程执行。多的扩展就不说了。
互斥与信号量本质区别
可以看到互斥和信号量在信号量为0和1的时候非常的类似,那么互斥与信号量到底有什么区别呢?
直接一句话说本质就是,这两个操作的目的不同。
信号量解决的问题是线程之间的调度,比如说线程C要在线程A和线程B之后执行,那么只需要在线程A和线程B中嵌入信号量即可,线程A和线程B对信号量增加,线程C等待线程A和线程B将信号量释放后才执行。就实现了信号量对线程的调度。
而互斥所要解决的问题是多个线程同时访问共享资源的时候,只能有一个线程操作的问题。
所以网上其他说的区别都是基于以上的区别所说明的,只要记住两者的目的不同即可
扩:互斥与条件变量
在说到互斥锁的时候就不得不提一下条件变量这个东西。
首先说一下条件变量所要解决的业务场景。
有三个线程对共享资源X(初始值为0)进行操作,线程A和线程B对X进行自增,每次都加一,而线程C需要判断如果X大于100,就将共享资源重新设置为0。
线程A和线程B好说,直接加锁后,对X进行加一,后进行解锁就行了。
但是对于线程C来说就比较麻烦了,线程C首先有个死循环,不停的去获取锁,对X进行判断,如果大于100就设置为0,后释放锁,如果不大于100,则释放锁后,sleep一段时间。
这时就会有两个问题:
- 共享资源X大多数的情况下是不会到达100的,其实线程C大多数情况下sleep的,无形中会浪费CPU处理时间。
- 但是为了节省CPU处理时间,可能会增加sleep的时间,减少自旋时间,但是又会产生时间过长,共享资源X都已将增加到了200时才醒过来。
所以为了解决在这种场景下所产生的这两个问题,产生出了条件变量这个东西。
就是在线程C里面加上一个等待条件,在线程A和线程B中增加触发这个等待条件的操作,就是线程C中增加等待X达到100的这个条件,而在线程A和线程B中增加判断,如果自增之后发现已将到底100了,去唤醒线程C,这时线程C知道了X已经大于100了,再将X设置为0
以上就是对条件变量的解释,其实还挺简单的,下面对应一段伪代码解释一下。
伪代码解释条件变量
线程A和线程B都对共享变量进行增加
{
pthread_mutex_lock(lock_s);
X++;
pthread_mutex_unlock(lock_s);
}
没有条件变量的时候,线程C
{
while (true)
{
pthread_mutex_lock(lock_s);
if(X>100)
{
X = 0;
pthread_mutex_unlock(lock_s);
}
else
{
pthread_mutex_unlock(lock_s);
my_thread_sleep(100);
}
}
}
但是如果有条件变量的时候
线程A和线程B在生成完数据后需要增加判断
{
pthread_mutex_lock(lock_s);
X++;
pthread_mutex_unlock(lock_s);
if(X>=100)
pthread_cond_signal(&cond_sum_ready);
}
线程C进行等待
{
pthread_mutex_lock(lock_s);
while(X>100)
pthread_cond_wait(&cond_sum_ready, &lock_s);
X=0;
pthread_mutex_unlock(lock_s);
}
补充:
- 在调用pthread_cond_wait这个方法的时候,会让拿到锁的线程暂时释放锁,然后休眠,等待条件达成通知,收到通知后会重新获取到锁。否则的话,线程C会一直持有共享资源X的互斥锁,导致其他线程不能执行。
- 为什么在线程C中还需要添加while循环,这时因为线程A或者B在调用pthread_cond_signal通知的时候,pthread_cond_wait可能不会立即执行,会有个时间差,假如这个时候有个线程D又把X减少到了50,这时如果设置为0就会产生程序冲突,所以这个用while循环再次检查X的大小。