书:Go语言学习笔记

当设置为1时不存在资源竞争

runtime.GOMAXPROCS(1)

在具有一个内核线程M(1)和一个逻辑处理器(P)的Go程序中,如果正在执行的M被syscall阻塞,则运行时会请求与P数量相同的“Spinning Threads”以允许等待的可运行goroutine继续执行。因此,在此期间,内核线程的数量M将大于P的数量(自旋线程+阻塞线程)。因此,即使将runtime.GOMAXPROCS的值设置为1,程序也将处于多线程状态。

自旋锁

何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。

  • 过多占用CPU资源

  • 用于锁使用者保持锁时间比较短的情况,如果需要长时间锁定的话, 最好使用信号量

  • 被保护的共享资源需要在中断上下文访问

  • 对多处理器相当有效的机制

互斥公平性。
互斥可以有两种操作模式:正常和饥饿。
在正常模式下,goroutine按照先进先出的顺序排队,但是被唤醒的goroutine并不拥有互斥锁,而是与新到达的goroutine竞争所有权。
新来的goroutine有一个优势——已经在CPU上运行,而且可能有很多,所以一个醒来的goroutine很有可能会失败。
在这种情况下,它在等待队列的前面排队。如果锁请求超过1毫秒未能获取互斥体,它会将互斥模式切换到饥饿模式。
在饥饿模式下,互斥锁的所有权直接从解锁的goroutine中移交给队列前面的锁请求。
新到达的goroutines不会尝试获取互斥锁,即使它看起来是未锁定的,也不会尝试自旋。相反,它们将自己排在等待队列的末尾。
如果goroutine获得了互斥锁的所有权,并且看到以下两种情况:
(1)它是队列中的最后一个goroutine
(2)它等待了不到1 ms
它将互斥体切换回正常操作模式。
正常模式的性能要好得多,因为即使有阻塞的等待者,goroutine也可以连续多次获取互斥体。
饥饿模式非常重要的,因为它能阻止尾部延迟的现象。

C:\Go\src\runtime\proc.go

func sync_runtime_canSpin(i int) bool {
	// sync.Mutex is cooperative, so we are conservative with spinning.
	// Spin only few times and only if running on a multicore machine and
	// GOMAXPROCS>1 and there is at least one other running P and local runq is empty.
	// As opposed to runtime mutex we don't do passive spinning here,
	// because there can be work on global runq or on other Ps.
	if i >= active_spin || ncpu <= 1 || gomaxprocs <= int32(sched.npidle+sched.nmspinning)+1 {
		return false
	}
	if p := getg().m.p.ptr(); !runqempty(p) {
		return false
	}
	return true
}