Golang又一个和RLock有关的小故事
- 2019 年 12 月 26 日
- 笔记
一. 背景
前几天看了一篇golang的文章一个和RLock有关的小故事, 发现作者得到的结论是错误的, 实际涉及内容比作者讲解的多一些。
二. 错误结论
先看下面的代码, 此段代码表现为sync.RWMutex的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() }
某一次的控制台输出样式:

整个代码片只涉及RWMutex的RLock和RUnlock, RLock是不相互影响的, 我们可以理解为只涉及atomic.AddInt32。
但是代码执行过程偶尔打印"fuck here", 表明某些时候RLock耗时超过100ms, 这对我们来说应该是impossible。

作者查看某个grontinue表明是syscall blocking, 其实是正常的调度。

作者用go tool对trace分析得到Goroutine执行结果统计, 看到goroutine存在blocking syscall就武断的认为是syscall原因引起
作者解释原因是:goroutine在RLock后, 因为要调用time.Since(time.Now)的syscall被runtime调出, 后随缘被系统再次调度, 这个随缘可能耗时超过100ms. 但是这个结论50%是错误的, 50%是正确的 。
三. time.Now()的实现
golang的time.Now最终调用是用汇编实现的代码, 代码精简后如下。
我们从代码中可以发现得到时间用的的vdso方式的调用, 因为有些内核使用太频繁, 每次都内核调用开销太高, 就将用户态的一段内存映射到内核, 这样内核调用就转换成用户态函数调用和内存读取。
// func walltime() (sec int64, nsec int32) TEXT runtime·walltime(SB), NOSPLIT, $0-12 ... MOVQ g(CX), AX MOVQ g_m(AX), BX // BX unchanged by C code. MOVQ 0(SP), DX MOVQ DX, m_vdsoPC(BX) LEAQ sec+0(SP), DX MOVQ DX, m_vdsoSP(BX) CMPQ AX, m_curg(BX) // Only switch if on curg. ... noswitch: ... MOVQ runtime·vdsoClockgettimeSym(SB), AX ... MOVQ $0, m_vdsoSP(BX) ... fallback: ... MOVQ runtime·vdsoGettimeofdaySym(SB), AX ... MOVQ $0, m_vdsoSP(BX) ...
walltime涉及的vdso库的解析.
var vdsoSymbolKeys = []vdsoSymbolKey{ {"__vdso_gettimeofday", 0x315ca59, 0xb01bca00, &vdsoGettimeofdaySym}, {"__vdso_clock_gettime", 0xd35ec75, 0x6e43a318, &vdsoClockgettimeSym}, }
当然上面代码阅读起来还是有点困难, 进行翻译以后就是下面的代码, vdso_clock_gettime的精度是纳秒而 vdso_gettimeofday的精度是微秒。
type timespec struct { sec int64 nsec int64 } type timeval struct { sec int64 usec int64 } func walltime() (sec int64, nsec int32) { if __vdso_clock_gettime != nil { t := ×pec{} __vdso_clock_gettime(CLOCK_REALTIME, t) return t.sec, int32(t.nsec) } t := &timeval{} __vdso_gettimeofday(t, nil) return t.sec, int32(t.usec * 1000) }
在walltime确实涉及到GPM中G和M的调度m_curg, 但是这个只是使用线程M的调度器G0的栈而已, 只因为G0的栈比普通的栈大, 而vdso调用需要的栈比较大而已。
四. syscall blocking
通过上面的分析可以看出整个time.Now()操作都是同步完成, 不涉及syscall blocking。
那文章作者的syscall blocking怎么来的呢?我们可以在本地跑一下代码, 我们选择个配置比较差的环境跑程序, trace后查看具体的阻塞goroutine的event。

生成syscall blocking图如下

可以看到这个syscall blocking是因为这段代码调用标准输出打印信息导致, 追踪fmt.Println("fuck here")最终定位到下面的代码, 在代码里边存在.
func WriteFile(handle Handle, buf []byte, done *uint32, overlapped *Overlapped) (err error) { var _p0 *byte if len(buf) > 0 { _p0 = &buf[0] } r1, _, e1 := Syscall6(procWriteFile.Addr(), 5, uintptr(handle), uintptr(unsafe.Pointer(_p0)), uintptr(len(buf)), uintptr(unsafe.Pointer(done)), uintptr(unsafe.Pointer(overlapped)), 0) if r1 == 0 { if e1 != 0 { err = errnoErr(e1) } else { err = EINVAL } } return }
函数调用的都是syscall一簇函数系列, 此系列函数还有Syscall/RawSyscall/RawSyscall6等, 这些函数在runtime中都是汇编实现, 我们抽取其中一个来进行分析, 这里边最关键的就是runtime·entersyscall和runtime·exitsyscall两个函数。
TEXT ·Syscall(SB), NOSPLIT, $0-56 CALL runtime·entersyscall(SB) ... SYSCALL ... CALL runtime·exitsyscall(SB) RET ok: ... CALL runtime·exitsyscall(SB) RET
- runtime·exitsyscall粗略执行逻辑:通知系统调用时候,是会将grontinue的M的P解绑,P可以去继续获取M执行其余的是会将grontinue的M的P解绑,这样提升效率。
- runtime·exitsyscall粗略执行逻辑: 通知调度系统, 解除grontinue与M的绑定, 将M加入到空闲队列, 将grontinue放入到全局调度队列中。
从上面的分析可以得出结论是WriteFile调用Syscall6导致syscall blocking。
五. 正确结论
在grontinue执行到下面函数中间时候被调去, 然后随缘调度, 等调度回来的时候超过100ms。
mlock.RLock() //here tmp1 := time.Since(beginTime).Nanoseconds() / 1000000
六. 真正syscall blocking实验
我们来个mlock.RLock()后真实存在需要syscall blocking的操作来测试一下。
因为比较喜欢window系统所以选择window下的os.Getenv来测试。
注意在linux下os.Getenv实现和window不同。
func GetEnvironmentVariable(name *uint16, buffer *uint16, size uint32) (n uint32, err error) { r0, _, e1 := Syscall(procGetEnvironmentVariableW.Addr(), 3, uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(buffer)), uintptr(size)) n = uint32(r0) if n == 0 { if e1 != 0 { err = errnoErr(e1) } else { err = EINVAL } } return }
测试的部分代码逻辑如下, 我们只选择创建3个goroutine, 100个goroutine信息太多图会生成失败。
var buf bytes.Buffer func get(i int) { mlock.RLock() env:=os.Getenv("aaa") if env!=""{ env="" } buf.WriteString(env) mlock.RUnlock() }
生成的goroutine调度远比没有syscall blocking的复杂。

生成syscall blocking图如下:
