2-1.runtime.sema信号量
sync
包下的结构体或多或少都使用了信号量机制,因此先介绍信号量
信号量
信号量是并发编程中常见的一种同步机制,在需要控制访问资源的线程数量时就会用到信号量。其实信号量就是一种变量或者抽象数据类型,用于控制并发系统中多个进程对公共资源的访问,访问具有原子性。信号量主要分为两类:
二进制信号量
binary semaphore
:其值只有两种0
或者1
,相当于互斥量,当值为1
时资源可用,当值为0
时,资源被锁住,进程阻塞无法继续执行。在
Linux
系统中,二进制信号量又称互斥锁Mutex
。
计数信号量 / 一般信号量
counting semaphore / general semaphore
:信号量是一个任意的整数,起始时,如果计数器的计数值为0
,那么创建出来的信号量就是不可获得的状态,如果计数器的计数值大于0
,那么创建出来的信号量就是可获得的状态,并且总共获取的次数等于计数器的值。
信号量是由操作系统来维护的,信号量只能进行两种操作:等待和发送信号,即 PV
操作:
P
原语:P
是荷兰语Proberen
(测试) 的首字母。为阻塞原语,负责把当前进程由运行状态转换为阻塞状态,直到另外一个进程唤醒它。操作为:申请一个空闲资源 (把信号量减1),若成功,则退出;若失败,则该进程被阻塞;V
原语:V
是荷兰语Verhogen
(增加) 的首字母。为唤醒原语,负责把一个被阻塞的进程唤醒,它有一个参数表,存放着等待被唤醒的进程信息。操作为:释放一个被占用的资源 (把信号量加1),如果发现有被阻塞的进程,则选择一个唤醒之。
在信号量进行 PV
操作时都为原子操作,并且在 PV
原语执行期间不允许有中断的发生。
PV
原语对信号量的操作可以分为三种情况:
把信号量视为某种类型的共享资源的剩余个数,实现对一类共享资源的访问
视信号量为一个加锁标志,实现对一个共享变量的访问
把信号量用作进程间的同步
运行方式:
初始化信号量,给与它一个非负数的整数值。
运行
P
,即wait
,信号量S
的值将被减少。企图进入临界区的进程(必须申请锁的进程),需要先运行wait()
。当信号量S
减为负值时,进程会被阻塞住,不能继续;当信号量S
不为负值时,进程可以获准进入临界区。运行
V
,即signal
,信号量S
的值会被增加。结束离开临界区的进程(比如释放锁的进程),将会运行signal()
,释放信号量。当信号量S
不为负值时,先前被阻塞住的其他进程,将可获准进入临界区。
我们一般用信号量保护一组资源,比如数据库连接池、一组客户端的连接等等。每次获取资源时都会将信号量中的计数器减去对应的数值,在释放资源时重新加回来。当遇到信号量资源不够时尝试获取的线程就会进入休眠,等待其他线程释放归还信号量。如果信号量是只有0和1的二进位信号量,那么,它的 P/V
就和互斥锁的 Lock/Unlock
一样了。
Go 中的 sema
sema
是 Go
实现的一个类似于 Futex
的信号量库,用于阻塞和唤醒协程。主要包含三个字段:一个整数用作锁,一个树堆作为队列存储阻塞的协程,和一个整数存储阻塞的协程数量
树堆:普通的二叉搜索树可能存在平衡问题(一边深一边浅,甚至可能退化为一个链表),因此给数上的每个节点随机赋一个权重,根据这个权重旋转二叉树,把它调整为一个最大或最小堆
为啥用树堆而不是链表或数组做队列存储等待的协程呢?
因为要插入的协程可能已经存在于队列里了,为了快速查找插入位置,树里按照地址排序
PV
相关的函数为:
semacquire1()
加锁时调用,对应P操作。申请资源,阻塞休眠当前协程semrelease1()
解锁时调用,对应V操作。释放资源,唤醒等待的协程
核心逻辑
这里简单看下 semacquire1()
函数的逻辑:
先 CAS 修改信号量,修改成功则视为成功获取资源
否则将当前协程放入树里,并使用
gopark
休眠当前协程
Last updated