Golang Go语言中 Runtime: 锁的阻塞方式

发布于 1周前 作者 sinazl 来自 Go语言

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

  • 自旋(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 &lt; passive_spin; i++ {
		for l.key == mutex_unlocked {
			if atomic.Cas(key32(&amp;l.key), mutex_unlocked, wait) {
				return
			}
		}
		osyield()
	}

	// Sleep.
	v = atomic.Xchg(key32(&amp;l.key), mutex_sleeping)
	if v == mutex_unlocked {
		return
	}
	wait = mutex_sleeping
	futexsleep(key32(&amp;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


Golang Go语言中 Runtime: 锁的阻塞方式

更多关于Golang Go语言中 Runtime: 锁的阻塞方式的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

好像从 v2 骗了波流量,难得啊

更多关于Golang Go语言中 Runtime: 锁的阻塞方式的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在Go语言中,runtime 包负责底层的运行时管理,包括内存分配、垃圾回收以及并发执行等。关于锁的阻塞方式,Go 采用了多种策略来优化并发性能,减少锁的争用和等待时间。

Go 的锁机制主要通过 sync 包中的 MutexRWMutex 实现。这些锁在底层依赖于操作系统的原子操作和调度机制。当多个 goroutine 尝试获取同一个锁时,未获取到锁的 goroutine 会被阻塞,直到锁被释放。

Go 运行时通过 goroutine 调度器来管理这些被阻塞的 goroutine。当锁被持有时,等待锁的 goroutine 会被挂起,并放入调度器的等待队列中。一旦锁被释放,调度器会唤醒一个或多个等待中的 goroutine,并尝试重新获取锁。

为了减少锁的阻塞和上下文切换的开销,Go 鼓励使用更细粒度的锁和通道(channel)等并发原语。此外,Go 的运行时还提供了诸如 sync.WaitGroupsync.Once 等工具,帮助开发者更高效地管理并发任务。

值得注意的是,Go 的垃圾回收器也会与运行时锁机制交互,以确保在并发环境中安全地进行内存回收。在垃圾回收过程中,可能会暂时暂停所有 goroutine(即 STW,Stop The World),以减少数据不一致的风险。然而,Go 团队一直在努力优化垃圾回收算法,以减少 STW 的时间和频率。

总之,Go 语言的锁阻塞方式是一个复杂而高效的机制,旨在提供高性能的并发编程体验。

回到顶部