一个和 RLock 有关的小故事
同事给了一个挺有意思的程序:
package main
import (
"fmt"
"os"
"runtime/trace"
"sync"
"time"
)
var mlock sync.RWMutex
var wg sync.WaitGroup
func main() {
trace.Start(os.Stderr)
defer trace.Stop()
wg.Add(100)
for i := 0; i < 100; i++ {
go gets()
}
wg.Wait()
}
func gets() {
for i := 0; i < 100000; i++ {
get(i)
}
wg.Done()
}
func get(i int) {
beginTime := time.Now()
mlock.RLock()
tmp1 := time.Since(beginTime).Nanoseconds() / 1000000
if tmp1 > 100 { // 超过100ms就打印出来
fmt.Println("fuck here")
}
mlock.RUnlock()
}
并表示这里全是 RLock,按理说超过 100ms 似乎不太合理。
RLock 本身是一个很轻量的操作(atomic.AddInt32),不太可能会出现超出加个 RLock 超过 100ms 情况出现,除非和写锁频繁冲突。
在命令行执行 go run back2.go 2> trace
,发现还是可能出现 **** here
。
多运行几次,在控制台出现关键词时(记住这里出现的次数),进入 go tool trace ./trace
。
会看到有 goroutine 有在 blocking syscall 上被阻塞过:
相应的 goroutine 是在 time.Since 调用这里被调度调出了,被调出的 goroutine 实际上仍然持有读锁。读锁之间是相容的,所以没有看到其它的 g 在 trace 上被同步手段所阻塞,因为其可以正常的 atomic 修改 reader 计数,从整体的 trace 图上也可以验证:
goroutine 的运行还是比较密集的。
倘若这里有写锁,那可能就杯具了。
同步+密集循环+调度,导致了这里的延时不可控。
此外,在 trace 的统计表格中,出现 blocking syscall > 0
的 goroutine 数和命令行中出现关键词的行数也是对得上的。因此可以简单地验证我们的结论,即这些 goroutine 并不是获取锁慢,是因为调获取时间的 syscall 被 runtime 调出了。。。被调出的 g 什么时候才能程序获得执行权只能随缘了。
结论:写代码的话,Lock 内尽量不要有 syscall(哪怕是 time.Now,time.Since 这种计时行为),RLock 也一样(虽然有时候确实避免不了)。