开头
- 如果一部分程序会被并发访问或修改,那么,为了避免并发访问导致的意向不到的结果,这部分程序需要被保护起来,这部分程序就是临界区。
- 锁的设计是为了保证多线程情况下,限定临界区同一时间只能有1个线程持有。
- 通常锁都是排他的。当某个线程获得锁后,其他线程需要在这个线程释放该锁或允许介入时才可进入临界区。
自旋锁
基础知识
- 属于busy-waiting类型的锁。
- 当一个线程对资源加锁后,其他线程需要锁时会一直检测锁状态尝试进行锁的获取,且不会释放CPU,直到拿到锁为止。这种锁等待方式也称 旋转 或 忙等待。
- 因为避免了进程上下文的调度开销,所以属于轻量级的锁,开销小。
场景举例
如果线程A去请求锁,那么线程A就会一直在Core0上进行忙等待并不停的进行锁请求,直到得到这个锁为止。
适用场景
- 适用于锁使用者保持锁时间比较短的情况下。
- 正是由于自旋锁使用者一般保持锁时间非常短,其他线程不需要睡眠是非常合理的,所以自旋锁的效率远高于互斥锁。适合抢占式任务,所以有可能存在有的协程等待时间过长,出现线程饥饿。
- 自旋锁一直占用着CPU,他在未获得锁的情况下,一直运行(自旋),如果不能在很短的时间内获得锁,这无疑会使CPU效率降低,因此我们要慎重使用自旋锁。
- 自旋锁只有在内核可抢占式或SMP(多处理器)的情况下才真正需要,在单CPU且不可抢占式的内核下,自旋锁的操作为空操作。单cpu的时候自旋锁会让其它线程动不了. 因此,一般自旋锁实现会有一个参数限定最多持续尝试次数. 超出后, 自旋锁放弃当前time slice. 等下一次机会。
- 在使用递归时容易造成死锁。所以递归程序决不能在持有自旋锁时调用它自己。
go 源码暂无可直接使用的自旋锁。
互斥锁 - Mutex
基础知识
- 属于sleep-waiting类型的锁。独占锁,该锁也叫做全局互斥锁 或 排它锁。
- 当线程获取锁失败时释放CPU,由系统调度转到执行其它线程任务。所以存在较大的切换上下文开销。
场景举例
例如在一个双核的机器上有两个线程(线程A和线程B),它们分别运行在Core0和Core1上。假设线程A想要去得到一个临界区的锁,而此时这个锁正被线程B所持有,那么线程A就会被阻塞,Core0会在此时进行上下文切换(Context Switch)将线程A置于等待队列中,此时Core0就可以运行其它的任务而不必进行忙等待。
适用场景
- 适用于占用时间较长的场景,当获取不到锁时此线程会进入睡眠,等待锁释放时被唤醒(闲等)。此时CPU可以激活其他线程进行工作。
- 当获取锁操作失败时,线程
- 对比读写锁,适用于读写不确定场景,即读写次数没有明显的区别,并且只允许只有一个读或者写的场景。
解析源码
//零值是未锁定的互斥锁, 首次使用后不得复制互斥锁。type Mutex struct {state int32sema uint32}//Locker表示可以锁定和解锁的对象。type Locker interface {Lock()Unlock()}//加锁,锁定当前的互斥量func (m *Mutex) Lock()//解锁,对当前互斥量进行解锁func (m *Mutex) Unlock()
关键知识点
- 在Lock()和Unlock()之间的代码段称为资源的临界区(critical section),在这一区间内的代码是严格被Lock()保护的,是线程安全的,任何一个时间点都只能有一个goroutine执行这段区间的代码。
- 锁定的互斥锁与特定的goroutine无关,允许一个goroutine锁定Mutex然后安排另一个goroutine来解锁它。
- 只有Lock()与Lock()之间才会导致阻塞的情况,Mutex一旦被锁住,其它的Lock()操作就无法再获取它的锁,只有通过Unlock()释放锁之后才能通过Lock()继续获取锁。也就是说,已有的锁会导致其它申请Lock()操作的goroutine被阻塞,且只有在Unlock()的时候才会解除阻塞。
- 如果在Unlock()时还未Lock(),则会引发panic错误,且recover无法捕获。
栗子
1. defer 的使用
不像C或Java的锁类工具,我们可能会犯一个错误:忘记及时解开已被锁住的锁,从而导致流程异常。但Go由于存在defer,所以此类问题出现的概率极低。关于defer解锁的方式如下: ```go // 声明一个互斥锁 var mutex sync.Mutex
func Write() { mutex.Lock() defer mutex.Unlock() }
<a name="qLDhH"></a>#### 2. 正确操作```gopackage mainimport("fmt""time""sync""math/rand")var lock sync.Mutexfunc main() {testMap()}func testMap() {var a map[int]inta = make(map[int]int, 5)a[8] = 10a[3] = 10a[2] = 10a[1] = 10a[18] = 10for i := 0; i < 2; i++ {go func(b map[int]int) {lock.Lock()b[8] = rand.Intn(100)lock.Unlock()}(a)}lock.Lock()fmt.Println(a)lock.Unlock()time.Sleep(time.Second)fmt.Println(a)}
输出如下
map[3:10 2:10 1:10 18:10 8:10]map[2:10 1:10 18:10 8:87 3:10]
我们利用了time.Sleep(time.Second)这个进行的延迟,goroute执行完毕,就进行输出,结果是进行了map的修改
2. 锁阻塞
对一个已经上锁的对象再次上锁,那么就会导致该Goroutine被阻塞,直到该互斥锁回到被解锁状态
func main() {var mutex sync.Mutexfmt.Println("start lock main")mutex.Lock()fmt.Println("get locked main")for i := 1;i<=3 ;i++ {go func(i int) {fmt.Println("start lock ",i)mutex.Lock()fmt.Println("get locked ",i)}(i)}time.Sleep(time.Second)fmt.Println("Unlock the lock main")mutex.Unlock()fmt.Println("get unlocked main")time.Sleep(time.Second)}
我们在for循环之前开始加锁,然后在每一次循环中创建一个协程,并对其加锁,但是由于之前已经加锁了,所以这个for循环中的加锁会陷入阻塞直到main中的锁被解锁, time.Sleep(time.Second) 是为了能让系统有足够的时间运行for循环,输出结果如下:
start lock mainget locked mainstart lock 3start lock 1start lock 2Unlock the lock mainget unlocked mainget locked 3
最终在main解锁后,三个协程会重新抢夺互斥锁权,最终协程3获胜。其它的Lock()将争夺互斥锁,也就是所谓的竞争现象(race condition)。因为竞争的存在,这几个个critical section被访问的顺序是随机的,完全无法保证哪个critical section先被访问。
3. 解锁时未加锁引发panic
互斥锁锁定操作的逆操作并不会导致协程阻塞,但是有可能导致引发一个无法恢复的运行时的panic,比如对一个未锁定的互斥锁进行解锁时就会发生panic。避免这种情况的最有效方式就是使用defer。我们知道如果遇到panic,可以使用recover方法进行恢复,但是如果对重复解锁互斥锁引发的panic却是徒劳的(Go 1.8及以后)。
func main() {defer func() {fmt.Println("Try to recover the panic")if p := recover();p!=nil{fmt.Println("recover the panic : ",p)}}()var mutex sync.Mutexfmt.Println("start lock")mutex.Lock()fmt.Println("get locked")fmt.Println("unlock lock")mutex.Unlock()fmt.Println("lock is unlocked")fmt.Println("unlock lock again")mutex.Unlock()}
以上代码试图对重复解锁引发的panic进行recover,但是我们发现操作失败,输出结果:
start lockget lockedfatal error: sync: unlock of unlocked mutexunlock locklock is unlockedunlock lock againgoroutine 1 [running]:runtime.throw(0x4c2b46, 0x1e)C:/Go/src/runtime/panic.go:619 +0x88 fp=0xc04207dea8 sp=0xc04207de88 pc=0x428978sync.throw(0x4c2b46, 0x1e)C:/Go/src/runtime/panic.go:608 +0x3c fp=0xc04207dec8 sp=0xc04207dea8 pc=0x4288dcsync.(*Mutex).Unlock(0xc042060080)C:/Go/src/sync/mutex.go:184 +0xc9 fp=0xc04207def0 sp=0xc04207dec8 pc=0x456b59main.main()D:/GoDemo/src/MyGo/Demo_04.go:23 +0x1dd fp=0xc04207df88 sp=0xc04207def0 pc=0x48ca9druntime.main()C:/Go/src/runtime/proc.go:198 +0x20e fp=0xc04207dfe0 sp=0xc04207df88 pc=0x42a21eruntime.goexit()C:/Go/src/runtime/asm_amd64.s:2361 +0x1 fp=0xc04207dfe8 sp=0xc04207dfe0 pc=0x44f791
虽然互斥锁可以被多个协程共享,但还是建议将对同一个互斥锁的加锁解锁操作放在同一个层次的代码中。
4. 内置变量
其实,对于内置类型的共享变量来说,使用sync.Mutex和Lock()、Unlock()来保护也是不合理的,因为它们自身不包含Mutex属性。真正合理的共享变量是那些包含Mutex属性的struct类型。例如:
type mytype struct {m sync.Mutexvar int}x := new(mytype)
这时只要想保护var变量,就先x.m.Lock(),操作完var后,再x.m.Unlock()。这样就能保证x中的var字段变量一定是被保护的。
读写锁 - RWMutex
适用场景
- 读写锁实际是一种特殊的自旋锁,是针对读写操作的互斥锁,可分别针对读操作与写操作进行锁定和解锁操作。
- 这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有单个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。在这样的控制规则下,读写锁可以大大降低性能损耗。
解析源码
// 零值是未锁定的互斥锁。首次使用后,不得复制RWMutex。//如果goroutine持有RWMutex进行读取而另一个goroutine可能会调用Lock,那么在释放初始读锁之前,goroutine不应该期望能够获取读锁定。//特别是,这种禁止递归读锁定。 这是为了确保锁最终变得可用; 阻止的锁定会阻止新读操作获取锁定。type RWMutex struct {w Mutex //如果有待处理的写操作就持有writerSem uint32 // 写操作等待读操作完成的信号量readerSem uint32 //读操作等待写操作完成的信号量readerCount int32 // 读锁后面挂起了多少个写锁申请readerWait int32 // 已释放了多少个读锁}//对读操作的锁定func (rw *RWMutex) RLock()//对读操作的解锁func (rw *RWMutex) RUnlock()//对写操作的锁定func (rw *RWMutex) Lock()//对写操作的解锁func (rw *RWMutex) Unlock()// RLocker()用于返回一个实现了Lock()和Unlock()方法的Locker接口// 一次RUnlock()操作只是对读锁数量减1,即减少一次读锁的引用计数func (rw *RWMutex) RLocker() Locker
关键知识点
- RWMutex是基于Mutex的,在Mutex的基础之上增加了读、写的信号量,并使用了类似引用计数的读锁数量。
- Mutex还是RWMutex都不会和goroutine进行关联,这意味着它们的锁申请行为可以在一个goroutine中操作,释放锁行为可以在另一个goroutine中操作。
- 由于RLock()和Lock()都能保证数据不被其它goroutine修改,所以在RLock()与RUnlock()之间的,以及Lock()与Unlock()之间的代码区都是资源的临界区(critical section)。
- 如果不存在写锁,则Unlock()引发panic,如果不存在读锁,则RUnlock()引发panic,且recover无法捕获。
- 读锁与读锁兼容,写锁与写锁互斥,读锁与写锁互斥,只有在锁释放后才可以继续申请互斥的锁:
- 在读锁占用的情况下,同时允许申请多个读锁,阻止申请任何写锁。
- 在写锁占用的情况下,同时允许申请单个写锁,阻止申请任何读锁。
- 对于这两种锁类型,任何一个 Lock() 或 RLock() 均需要保证对应有 Unlock() 或 RUnlock() 调用与之对应,否则可能导致等待该锁的所有 goroutine 处于饥饿状态,甚至可能导致死锁。
栗子
1. 多个goroutine同时读
package mainimport("time""sync")var m sync.RWMutexfunc main() {// 多个同时读go read(1)go read(2)time.Sleep(2 * time.Second)}func read(i int) {println(i,"read start")m.RLock()println(i,"reading")time.Sleep(1*time.Second)m.RUnlock()println(i,"read over")}
输出如下,可以看出1 读还没有结束,2已经在读
PS E:\golang\go_pro\src\safly> go run demo.go1 read start1 reading2 read start2 reading1 read over2 read over
2. 正确操作
Unlock会试图唤醒所有因欲进行读锁定而被阻塞的协程,而 RUnlock 只会在已无任何读锁定的情况下,试图唤醒一个因欲进行写锁定而被阻塞的协程。若对一个未被写锁定的读写锁进行写解锁,就会引发一个不可恢复的panic,同理对一个未被读锁定的读写锁进行读写锁也会如此。
由于读写锁控制下的多个读操作之间不是互斥的,因此对于读解锁更容易被忽视。对于同一个读写锁,添加多少个读锁定,就必要有等量的读解锁,这样才能其他协程有机会进行操作。
func main() {var rwm sync.RWMutexfor i := 0; i < 3; i++ {go func(i int) {fmt.Println("try to lock read ", i)rwm.RLock()fmt.Println("get locked ", i)time.Sleep(time.Second *2)fmt.Println("try to unlock for reading ", i)rwm.RUnlock()fmt.Println("unlocked for reading ", i)}(i)}time.Sleep(time.Millisecond * 1000)fmt.Println("try to lock for writing")rwm.Lock()fmt.Println("locked for writing")}
上面的示例创建了三个协程用于对读写锁的读锁定与读解锁操作。在 rwm.Lock()种会对main中协程进行写锁定,但是for循环中的读解锁尚未完成,因此会造成mian中的协程阻塞。当for循环中的读解锁操作都完成后就会试图唤醒main中阻塞的协程,main中的写锁定才会完成。输出结果如下
try to lock read 0get locked 0try to lock read 2get locked 2try to lock read 1get locked 1try to lock for writingtry to unlock for reading 0unlocked for reading 0try to unlock for reading 2unlocked for reading 2try to unlock for reading 1unlocked for reading 1locked for writing
用Mutex还是用RWMutex
Mutex和RWMutex都不关联goroutine,但RWMutex显然更适用于读多写少的场景。仅针对读的性能来说,RWMutex要高于Mutex,因为rwmutex的多个读可以并存。
感谢
https://blog.csdn.net/u013210620/article/details/78357995
https://www.jianshu.com/p/94bdaf3ad125
https://www.cnblogs.com/f-ck-need-u/p/9998729.html
