锁的介绍
1 互斥锁
传统并发程序对共享资源进行访问控制的主要手段,由标准库代码包中 sync 中的 Mutex 结构体表示。
1 | //Mutex 是互斥锁, 零值是解锁的互斥锁, 首次使用后不得赋值互斥锁。 |
sync.Mutex 类型只有两个公开的指针方法
1 | //Locker表示可以锁定和解锁的对象。 |
声明一个互斥锁:
1 | var mutex sync.Mutex |
不像 C 或 Java 的锁类工具,我们可能会犯一个错误:忘记及时解开已被锁住的锁,从而导致流程异常。但 Go 由于存在 defer,所以此类问题出现的概率极低。关于 defer 解锁的方式如下:
1 | var mutex sync.Mutex |
如果对一个已经上锁的对象再次上锁,那么就会导致该锁定操作被阻塞,直到该互斥锁回到被解锁状态
1 | func main() { |
上面的示例中,我们在 for 循环之前开始加锁,然后在每一次循环中创建一个协程,并对其加锁,但是由于之前已经加锁了,所以这个 for 循环中的加锁会陷入阻塞直到 main 中的锁被解锁, time.Sleep(time.Second) 是为了能让系统有足够的时间运行 for 循环,输出结果如下:
1 | start lock main |
最终在 main 解锁后,三个协程会重新抢夺互斥锁权,最终协程 3 获胜。
互斥锁锁定操作的逆操作并不会导致协程阻塞,但是有可能导致引发一个无法恢复的运行时的 panic,比如对一个未锁定的互斥锁进行解锁时就会发生 panic。避免这种情况的最有效方式就是使用 defer。
我们知道如果遇到 panic,可以使用 recover 方法进行恢复,但是如果对重复解锁互斥锁引发的 panic 却是徒劳的(Go 1.8 及以后)。
1 | func main() { |
以上代码试图对重复解锁引发的 panic 进行 recover,但是我们发现操作失败,输出结果:
1 | start lock |
虽然互斥锁可以被多个协程共享,但还是建议将对同一个互斥锁的加锁解锁操作放在同一个层次的代码中。
2 读写锁
读写锁是针对读写操作的互斥锁,可以分别针对读操作与写操作进行锁定和解锁操作 。
读写锁的访问控制规则如下:
① 多个写操作之间是互斥的
② 写操作与读操作之间也是互斥的
③ 多个读操作之间不是互斥的
在这样的控制规则下,读写锁可以大大降低性能损耗。
由标准库代码包中 sync 中的 RWMutex 结构体表示
1 | // RWMutex是一个读/写互斥锁,可以由任意数量的读操作或单个写操作持有。 |
sync 中的 RWMutex 有以下几种方法:
1 | //对读操作的锁定 |
Unlock 会试图唤醒所有因欲进行读锁定而被阻塞的协程,而 RUnlock 只会在已无任何读锁定的情况下,试图唤醒一个因欲进行写锁定而被阻塞的协程。若对一个未被写锁定的读写锁进行写解锁,就会引发一个不可恢复的 panic,同理对一个未被读锁定的读写锁进行读写锁也会如此。
由于读写锁控制下的多个读操作之间不是互斥的,因此对于读解锁更容易被忽视。对于同一个读写锁,添加多少个读锁定,就必要有等量的读解锁,这样才能其他协程有机会进行操作。
1 | func main() { |
上面的示例创建了三个协程用于对读写锁的读锁定与读解锁操作。在 rwm.Lock()种会对 main 中协程进行写锁定,但是 for 循环中的读解锁尚未完成,因此会造成 mian 中的协程阻塞。当 for 循环中的读解锁操作都完成后就会试图唤醒 main 中阻塞的协程,main 中的写锁定才会完成。输出结果如下
1 | try to lock read 0 |