这是我参与「第五届青训营 」伴学笔记创作活动的第 2天
一、Mutex(互斥锁)
Mutex是互斥锁的意思,也叫排他锁,同一时刻一段代码只能被一个线程运行,使用只需要关注方法Lock(加锁)和Unlock(解锁)即可。
在Lock()和Unlock()之间的代码段称为资源的临界区(critical section),是线程安全的,任何一个时间点都只能有一个goroutine执行这段区间的代码。
不加锁示例
先来一段不加群的代码,10个协程同时累加1万
package main
import (
"fmt"
"sync"
)
func main() {
var count = 0
var wg sync.WaitGroup
//十个协程数量
n := 10
wg.Add(n)
for i := 0; i < n; i++ {
go func() {
defer wg.Done()
//1万叠加
for j := 0; j < 10000; j++ {
count++
}
}()
}
wg.Wait()
fmt.Println(count)
}
复制代码
运行结果如下
38532
复制代码
正确的结果应该是100000,这里出现了并发写入更新错误的情况
加锁示例
我们再添加锁,代码如下
package main
import (
"fmt"
"sync"
)
func main() {
var count = 0
var wg sync.WaitGroup
var mu sync.Mutex
//十个协程数量
n := 10
wg.Add(n)
for i := 0; i < n; i++ {
go func() {
defer wg.Done()
//1万叠加
for j := 0; j < 10000; j++ {
mu.Lock()
count++
mu.Unlock()
}
}()
}
wg.Wait()
fmt.Println(count)
}
复制代码
可以看到结果变成了正确的100000
二、RWMutex(读写锁)
Mutex在大量并发的情况下,会造成锁等待,对性能的影响比较大。
如果某个读操作的协程加了锁,其他的协程没必要处于等待状态,可以并发地访问共享变量,这样能让读操作并行,提高读性能。
RWLock就是用来干这个的,这种锁在某一时刻能由什么问题数量的reader持有,或者被一个wrtier持有
主要遵循以下规则 :
- 读写锁的读锁可以重入,在已经有读锁的情况下,可以任意加读锁。
- 在读锁没有全部解锁的情况下,写操作会阻塞直到所有读锁解锁。
- 写锁定的情况下,其他协程的读写都会被阻塞,直到写锁解锁。
Go语言的读写锁方法主要有下面这种
- Lock/Unlock:针对写操作。
不管锁是被reader还是writer持有,这个Lock方法会一直阻塞,Unlock用来释放锁的方法 - RLock/RUnlock:针对读操作
当锁被reader所有的时候,RLock会直接返回,当锁已经被writer所有,RLock会一直阻塞,直到能获取锁,否则就直接返回,RUnlock用来释放锁的方法
并发读示例
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var m sync.RWMutex
go read(&m, 1)
go read(&m, 2)
go read(&m, 3)
time.Sleep(2 * time.Second)
}
func read(m *sync.RWMutex, i int) {
fmt.Println(i, "reader start")
m.RLock()
fmt.Println(i, "reading")
time.Sleep(1 * time.Second)
m.RUnlock()
fmt.Println(i, "reader over")
}
复制代码
三、死锁场景
当两个或两个以上的进程在执行过程中,因争夺资源而处理一种互相等待的状态,如果没有外部干涉无法继续下去,这时我们称系统处于死锁或产生了死锁。
死锁主要有以下几种场景。
Lock/Unlock不是成对出现
没有成对出现容易会出现死锁的情况,或者是Unlock 一个未加锁的Mutex而导致 panic,代码建议以下面紧凑的方式出现
mu.Lock()
defer mu.Unlock()