本文将讲解一下Go语言中的同步原语与锁。会阐述几种常见的锁,剖析其流程,然后针对每种同步原语举几个例子。由于文章比较长,为方便阅读,将这部分拆解为两部分。本文是第一部分 互斥锁与读写锁。
环境: go version go1.8.7 darwin/amd64
1 mutex
1.1 结构
先看下mutex在底层的结构: 代码位置sync/mutex.go
type Mutex struct {
state int32 // 状态
sema uint32 // 信号
}
在mutex中 主要有mutexLocked , mutexWorken 两种状态,mutexWaiterShift 是统计了等待mutex的routine 数量。
1.2 流程
参考文献中讲到Go1.9 针对互斥锁增加了饥饿模式,我们这儿不讨论了哈。
1.2.1 Lock
流程大体是这样子的:
1.2.2 Unlock
1.3 例子
package main
import (
"fmt"
"math/rand"
"sync"
"testing"
"time"
)
func TestMutex(t *testing.T) {
lock := sync.Mutex{}
var a map[int]int
a = make(map[int]int, 5)
a[8] = 10
a[3] = 10
a[2] = 10
a[1] = 10
a[18] = 10
for 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("main routine print in mutex......")
fmt.Println(a)
lock.Unlock()
time.Sleep(time.Second)
fmt.Println("main routine print at end......")
fmt.Println(a)
}
结论是:
main routine print in mutex......
map[8:10 3:10 2:10 1:10 18:10]
main routine print at end......
map[2:10 1:10 18:10 8:87 3:10]
2 RWmutex
RWmutex 读写锁,对共享资源的”读操作“ 和”写操作“ 进行了区分,更加细化。
其基本的原则是:
对某个受到读写锁保护的共享资源,多个写不能同时操作,读写也不可以同时操作,但是多个读可以同时进行。
2.1 结构
type RWMutex struct {
w Mutex // 写操作之间的互斥锁
writerSem uint32 // semaphore for writers to wait for completing readers
readerSem uint32 // semaphore for readers to wait for completing writers
readerCount int32 // 正在进行的读操作的数量
readerWait int32 // 当写操作被阻塞时等待的读操作个数
}
每个字段的含义在注释里面哈~
2.2 流程
这部分我们只做简单的流程分析。
2.2.1 读锁的获取
2.2.2 读锁的释放
2.2.3 写锁的获取
- 此时有读锁:进入休眠,等待被唤醒
- 此时有写锁:互斥,进入等待状态
- 此时没有读锁、写锁:获取写锁
2.2.3 写锁的释放
先释放写锁,然后看有没有在等待中的读操作:如果有读操作在等待,先让读锁执行;如果没有读操作,就释放写锁,让其他写锁获取当前锁。这样操作是防止读操作被饿死。
2.3 例子
先来个”读-读“的例子:
func TestRWMutex_multiRead(t *testing.T) {
m = new(sync.RWMutex)
// 多个同时读
now := time.Now().Nanosecond()
go read(1, now)
now2 := time.Now().Nanosecond()
go read(2,now2)
time.Sleep(2*time.Second)
}
func read(i int, beginTime int) {
println(time.Now().Nanosecond() - beginTime, i,"read start")
m.RLock()
println(time.Now().Nanosecond() - beginTime, i,"reading")
time.Sleep(1*time.Second)
println(time.Now().Nanosecond() - beginTime, i,"read over")
m.RUnlock()
}
结论是:
16000 1 read start
27000 1 reading
28000 2 read start
38000 2 reading
3159000 2 read over
3167000 1 read over
我们看到,reader1 没有结束,reader2就开始读了,验证了我们的读-读是可以并存的。
再来个”读-写混合“的例子:
func TestRWMutex_WriteRead(t *testing.T) {
m = new(sync.RWMutex)
beginTime := time.Now().Nanosecond()
// 有读有写: 读-写-写-读
go read(1, beginTime)
go write(2, beginTime)
go write(3, beginTime)
go read(4, beginTime)
time.Sleep(5*time.Second)
}
func read(i int, beginTime int) {
println(time.Now().Nanosecond() - beginTime, i,"read start")
m.RLock()
println(time.Now().Nanosecond() - beginTime, i,"reading")
time.Sleep(1*time.Second)
println(time.Now().Nanosecond() - beginTime, i,"read over")
m.RUnlock()
}
func write(i int, beginTime int) {
println(time.Now().Nanosecond() - beginTime, i,"write start")
m.Lock()
println(time.Now().Nanosecond() - beginTime, i,"writing")
time.Sleep(1*time.Second)
println(time.Now().Nanosecond() - beginTime, i,"write over")
m.Unlock()
}
看下执行某一次的结果:
10000 2 write start
24000 2 writing
42000 4 read start
44000 1 read start
20000 3 write start
1926000 2 write over
1955000 4 reading
1968000 1 reading
5641000 1 read over
5648000 4 read over
5714000 3 writing
10094000 3 write over
我们看到 先执行了写操作2,等到写操作2执行完毕之后,读操作4跟1才开始执行,等这俩读都执行完,最后才执行了写操作3
3 小结
本文是Go-同步原语与锁的第一部分: 互斥锁与读写锁。从结构、获取释放锁、例子三个维度进行了讲解。
4 参考文献
同步原语与锁 https://draveness.me/golang/concurrency/golang-sync-primitives.html
Go 1.8 源码
5 其他
本文是《循序渐进go语言》的第十三篇-《Go-同步原语与锁(一)互斥锁与读写锁》。
如果有疑问,可以直接留言,也可以关注公众号 “链人成长chainerup” 提问留言,或者加入知识星球“链人成长” 与我深度链接~
有疑问加站长微信联系(非本文作者)