V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
GopherDaily
V2EX  ›  Go 编程语言

Go Runtime: 锁的阻塞方式

  •  
  •   GopherDaily · 2023-11-02 16:34:35 +08:00 · 955 次点击
    这是一个创建于 390 天前的主题,其中的信息可能已经有所发展或是发生改变。

    从极端简化的角度来看, 线程阻塞于锁的方式有两种:

    • 自旋(spin)

      线程依然占有 CPU, 循环执行一些无意义的指令后, 检查状态, 如果可以持有锁则跳出循环. 适合预期短时间内即可获得锁的等待场景, 虽然空耗了 CPU, 但是避免了线程的上下文切换.

    • 睡眠(sleep)

      线程让出 CPU 的执行权, 进入等待队列. 可以持有锁后, 进入待执行队列. 适合预期要阻塞一段时间的场景, 虽然有上下文切换, 但允许 CPU 执行其他任务, 提高了整体吞吐.

    runtime.lock2 恰好涉及了上述两种场景.

    ...
    	// On uniprocessors, no point spinning.
    	// On multiprocessors, spin for ACTIVE_SPIN attempts.
    	spin := 0
    	if ncpu > 1 {
    		spin = active_spin
    	}
    	for {
    		// Try for lock, spinning.
    		for i := 0; i < spin; i++ {
    			for l.key == mutex_unlocked {
    				if atomic.Cas(key32(&l.key), mutex_unlocked, wait) {
    					return
    				}
    			}
    			procyield(active_spin_cnt)
    		}
    
    		// Try for lock, rescheduling.
    		for i := 0; i < passive_spin; i++ {
    			for l.key == mutex_unlocked {
    				if atomic.Cas(key32(&l.key), mutex_unlocked, wait) {
    					return
    				}
    			}
    			osyield()
    		}
    
    		// Sleep.
    		v = atomic.Xchg(key32(&l.key), mutex_sleeping)
    		if v == mutex_unlocked {
    			return
    		}
    		wait = mutex_sleeping
    		futexsleep(key32(&l.key), mutex_sleeping, -1)
    	}
    ...
    

    在多核的情况下, lock2 在需要阻塞时会优先尝试进行 4 次自旋. 自旋的实现和 CPU 有关, amd64 下会使用到 pause 这个指令.

    procyield:

    TEXT runtime·procyield(SB),NOSPLIT,$0-0
    	MOVL	cycles+0(FP), AX
    again:
    	PAUSE
    	SUBL	$1, AX
    	JNZ	again
    	RET
    

    osyield 对应系统调用 sched_yeild 仅让出 CPU 使用权, 但线程依然在待执行队列等待调度.

    futexsleep 对应睡眠, 底层系统调用是 futex. 调用成功后, 线程让出 CPU 使用权, 并变成等待状态, 直到 unlock2 中的 futexwake 被调用后, 线程才会重新放回待执行队列, 等到被调度后恢复执行.

    Go 使用 GMP 做为调度模型, 所以在除了上述的自旋和睡眠外, 我们还可以选择仅挂起 G, 让线程(M) 去执行其他 G. 这样我们既可以避免线程级别的上下文切换成本, 又可以避免无意义的占用 CPU.

    案例可以参考 semacquire1. goparkunlock 在释放 root.lock 之后, 将对应 G 的状态修改为 _Gwaiting, 并允许对应 M 执行其他 G.

    ...
    	for {
    		lockWithRank(&root.lock, lockRankRoot)
    		// Add ourselves to nwait to disable "easy case" in semrelease.
    		root.nwait.Add(1)
    		// Check cansemacquire to avoid missed wakeup.
    		if cansemacquire(addr) {
    			root.nwait.Add(-1)
    			unlock(&root.lock)
    			break
    		}
    		// Any semrelease after the cansemacquire knows we're waiting
    		// (we set nwait above), so go to sleep.
    		root.queue(addr, s, lifo)
    		goparkunlock(&root.lock, reason, traceBlockSync, 4+skipframes)
    		if s.ticket != 0 || cansemacquire(addr) {
    			break
    		}
    	}
    ...
    

    Link: https://github.com/j2gg0s/j2gg0s/tree/main/_posts

    GopherDaily
        1
    GopherDaily  
    OP
       2023-11-02 22:50:36 +08:00   ❤️ 1
    好像从 v2 骗了波流量,难得啊
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3793 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 05:04 · PVG 13:04 · LAX 21:04 · JFK 00:04
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.