自从前两天写了Go互斥锁实现原理后,我感觉我错了,我不应该看源码,看了也没啥用,会了也不敢用。按照源码中的写法,没人想做代码review,也不好做单元测试。
我就是想知道锁是怎么实现的,结果整一堆优化逻辑,感觉懂了又感觉啥都不懂。所以我痛定思痛,我看早期版本行吧。来让我们看2014年的版本
互斥锁:https://github.com/golang/go/blob/c007ce824d9a4fccb148f9204e04c23ed2984b71/src/sync/mutex.go
读写互斥锁:https://github.com/golang/go/blob/c007ce824d9a4fccb148f9204e04c23ed2984b71/src/sync/rwmutex.go
互斥锁
这个版本的互斥锁没有饥饿模式、没有自旋、没有各种小的性能优化点。但这段代码把最核心的逻辑展示的十分清楚。
说明
1 | type Mutex struct { |
state表示互斥锁的状态,比如是否被锁定等。
sema表示信号量,协程阻塞等待该信号量,解锁的协程释放信号量从而唤醒等待信号量的协程。
state的组成如下图所示:
Locked: 表示该Mutex是否已被锁定,0:没有锁定 1:已被锁定。
Woken: 表示是否有协程已被唤醒,0:没有协程唤醒 1:已有协程唤醒,正在加锁过程中。 释放锁时,如果正常模式下,不会再唤醒其它协程。
Waiter: 表示阻塞等待锁的协程个数,协程解锁时根据此值来判断是否需要释放信号量
因为在Go互斥锁实现原理已经写过很多基础知识了,这次只把核心逻辑、核心点写一下,如果对其中部分知识不太理解,可以看一下上一篇文章。
源码
1 | // Copyright 2009 The Go Authors. All rights reserved. |
关注点
1.Unlock中atomic.AddInt32(&m.state, -mutexLocked)是对m.state的指针真行操作,所以其实对m.state进行了真正的减一,这也是CAS操作时atomic.CompareAndSwapInt32(&m.state, old, new),m.state和old值可能一样的原因
2.大家课本上学到的信号量伪代码一般是这样的
1 | wait(S): while S<=0 do no-op; |
感觉和Mutex中的sema不一样。有这种疑问很正常,因为准确的说,Mutex才是真正意义上的信号量,Mutex中的sema虽然翻译为信号量,但其实只是为了将协程阻塞到以sema为地址的队列上,细节可查看semacquire1、semrelease1。
1 | type Mutex struct { |
课本上的伪代码:可以加锁,值减一;不可以加锁,死循环。
Mutex具体实现:可以加锁,Locked位置为1,不可以加锁,协程阻塞。
3.存在等待协程饿死的情况
读写锁
上一篇文章没有写读写锁,主要还是因为go1.13的代码阅读起来太过困难。这次看老版本的代码,简单的多,所以顺便写一下读写锁。
说明
读写锁结构体为:
1 | type RWMutex struct { |
- Mutex:复用写锁,应对读写锁变为写锁的情况。获得写锁首先要获取该锁,如果有一个写锁在进行,那么再到来的写锁将会阻塞于此
- writerSem:写阻塞等待的信号量,最后一个读者释放锁时会释放信号量
- readerSem:读阻塞的协程等待的信号量,持有写锁的协程释放锁后会释放信号量
- readerCount:记录读者个数
- readerWait:记录写阻塞时读者个数。表示加写锁的时候有几个读锁,当对应数量的读锁解锁完毕后,写锁开始被唤起。主要目的为防止写锁饿死。
源码
1 | // Copyright 2009 The Go Authors. All rights reserved. |
整个流程如下图
关注点
- 对于写锁,有两个阻塞。一是Mutex自身阻塞,另一个是RWMutex的writerSem阻塞。两者的作用不一样。Mutex用于处理写锁与写锁之间的关系,writerSem用于处理写锁与读锁之间的关系
- 源码使用readerWait记录请求写锁时读锁协程个数,当这些协程都释放锁,写锁加锁成功,防止写锁饿死
总结
通过阅读Go锁源码,明白真正的信号量实现逻辑。虽然整体思路上,和操作系统课本讲述是一致的,但是仍然有很多细节不一样。像Go没有选择空等,进行阻塞优化性能;像读写锁的实现要比整型信号量复杂的多。
对于源码的复杂性,我觉得分两个方面看。对于Go这种源码,一点点的性能优化,对于全球那么多使用者来说,收益是巨大的,所以可读性的重要性没那么高。如果是想学习的话,还是看一下早期的、可读性高一些的源码,在理解的基础上再看最新版本的源码,不但入门难度降低,而且能够思考别人是怎么进行思考、进行优化的。
对于具体业务而言,代码一定要有较高的可读性,不然code review的同学或者今后维护的同学,太痛苦!!!