在 Go 中,锁的主要作用是保护共享资源,避免多个 goroutine 同时访问导致数据不一致。常见的锁有 sync.Mutex
和 sync.RWMutex
。
sync.Mutex
源代码
type Mutex struct {
state int32 //state用来表示互斥锁的状态
sema uint32 //sema (semaphore)类型的信号量,用于控制等待锁的 goroutine
}
// Locker 表示可以被锁定和解锁的对象。
type Locker interface {
Lock()
Unlock()
}
const (
mutexLocked = 1 << iota // mutex is locked 1开始不是0 mutexLocked 用来表示互斥锁已经被锁定
mutexWoken //mutexWoken,表示互斥锁的等待者已经被唤醒,它的值是 1 << 1(2)。
mutexStarving //mutexStarving互斥锁处于饥饿模式,1 << 2(4)。
mutexWaiterShift = iota //mutexWaiterShift = iota 表示等待者数量的位从哪一位开始。因为这是 const 块的第四个声明,所以 iota 的值是 3,mutexWaiterShift 的值也是 3。这意味着在 state 变量中,等待者数量将从第 3 位开始计数。
// m.state 的状态位分布:
//mutexLocked(位 0):表示互斥锁是否被锁定。
// mutexWoken(位 1):表示互斥锁的等待者是否被唤醒。
// mutexStarving(位 2):表示互斥锁是否处于饥饿模式。
// mutexWaiterShift(位 3 及以后):表示等待者数量的位从第 3 位开始计数。
//下面是一个图像,展示了 m.state 的状态位分布:
// +--------+--------+--------+--------+--------+--------+--------+--------+
// | 31 ... | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
// | ...| waiter| waiter| waiter| mutexWoken | mutexStarving | mutexLocked |
// +--------+--------+--------+--------+--------+--------+--------+--------+
// 状态位示例
// 假设互斥锁被锁定,并且有 5 个 goroutine 在等待(即等待者数量为 5),同时互斥锁处于正常模式(非饥饿模式),m.state 的值可能是:
// +--------+--------+--------+--------+--------+--------+--------+--------+
// | 31 ... | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
// +--------+--------+--------+--------+--------+--------+--------+--------+
// | ... | 0 | 0 | 1 | 0 | 1 | 0 | 1 | 1 |
// +--------+--------+--------+--------+--------+--------+--------+--------+
// Mutex 公平性。
//
// Mutex 可以处于两种模式:正常和饥饿。
// 在正常模式下,等待者按 FIFO 顺序排队,但是唤醒的等待者并不拥有互斥锁,而是与新到达的 goroutine 竞争所有权。新到达的 goroutine 有优势——它们已经在 CPU 上运行,并且可能有很多,所以唤醒的等待者有很大的机会失去。在这种情况下,它会排在等待队列的前面。如果一个等待者超过 1ms 未能获得互斥锁,
// 它将切换到饥饿模式。
//
// 在饥饿模式下,互斥锁的所有权直接从解锁的 goroutine 传递给队列前面的等待者。
// 新到达的 goroutine 即使看起来互斥锁未锁定,也不会尝试获取互斥锁,也不会尝试自旋。相反,它们会将自己排在等待队列的末尾。
//
// 如果一个等待者获得了互斥锁的所有权,并看到要么
// (1) 它是队列中的最后一个等待者,或者 (2) 它等待的时间少于 1ms,
// 它将互斥锁切换回正常操作模式。
//
// 正常模式的性能明显更好,因为一个 goroutine 可以连续多次获取互斥锁,即使有被阻塞的等待者。
// 饥饿模式对于防止尾部延迟的病理情况很重要。
starvationThresholdNs = 1e6 //定义了饥饿模式的阈值,即如果一个等待者等待的时间超过 1 毫秒(1e6 纳秒),互斥锁将切换到饥饿模式。
)
// Lock 加锁 m。
// 如果锁已经被使用,调用 goroutine
// 会阻塞直到互斥锁可用。
func (m *Mutex) Lock() {
// Fast path: grab unlocked mutex.
// 快速路径:获取未锁定的互斥锁。
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
//如果 CompareAndSwapInt32 返回 true,则表示互斥锁已经被成功锁定,并且如果启用了竞争检测(通过 race 包),则调用 race.Acquire 以记录这次锁的获取。
if race.Enabled {
race.Acquire(unsafe.Pointer(m)) //?
}
return
}
// Slow path (outlined so that the fast path can be inlined)
// 慢速路径(为了使快速路径可以内联而单独列出)
m.lockSlow()
}
// TryLock 尝试锁定 m 并报告是否成功。
//
// 注意,尽管 TryLock 的正确用法确实存在,但它们很少见,
// 使用 TryLock 通常是对互斥锁使用方式的更深层次问题的信号。
func (m *Mutex) TryLock() bool {
old := m.state
//检查 state 是否已经被锁定或处于饥饿模式。如果是,则返回 false。
if old&(mutexLocked|mutexStarving) != 0 {
return false
}
// 可能有一个 goroutine 正在等待互斥锁,但我们正在运行,可以尝试在那个 goroutine 唤醒之前获取互斥锁。
if !atomic.CompareAndSwapInt32(&m.state, old, old|mutexLocked) {
return false
}
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
return true
}
race
检测器会输出一个报告,指出可能发生竞争的代码位置。如果没有数据竞争,程序将正常运行,不会输出任何关于 race 检测的信息。
Mutex 公平性。
Mutex 可以处于两种模式:正常和饥饿。
在正常模式下,等待者按 FIFO 顺序排队,但是唤醒的等待者并不拥有互斥锁,而是与新到达的 goroutine 竞争所有权。新到达的 goroutine 有优势——它们已经在 CPU 上运行,并且可能有很多,所以唤醒的等待者有很大的机会失去。在这种情况下,它会排在等待队列的前面。如果一个等待者超过 1ms 未能获得互斥锁,
它将切换到饥饿模式。
在饥饿模式下,互斥锁的所有权直接从解锁的 goroutine 传递给队列前面的等待者。
新到达的 goroutine 即使看起来互斥锁未锁定,也不会尝试获取互斥锁,也不会尝试自旋。相反,它们会将自己排在等待队列的末尾。
它会首先执行需要锁保护的代码。在执行完这些代码并准备释放互斥锁之前
如果一个等待者获得了互斥锁的所有权,并看到要么
(1) 它是队列中的最后一个等待者,或者 (2) 它等待的时间少于 1ms,
它将互斥锁切换回正常操作模式。
正常模式的性能明显更好,因为一个 goroutine 可以连续多次获取互斥锁,即使有被阻塞的等待者。
饥饿模式对于防止尾部延迟的病理情况很重要。
// runtime_canSpin 报告当前是否适合进行自旋。
//go:linkname sync_runtime_canSpin sync.runtime_canSpin
func sync_runtime_canSpin(i int) bool {
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
}
if i >= active_spin || ncpu <= 1 || gomaxprocs <= int32(sched.npidle+sched.nmspinning)+1 {
return false
}
active_spin
是一个阈值,表示在放弃自旋之前允许的最大自旋次数。如果当前的自旋次数i
大于或等于这个阈值,函数返回false
,表示不应该再自旋。ncpu
是系统中可用的 CPU 核心数。如果ncpu
小于或等于 1,意味着没有多个核心可以利用,因此自旋没有意义,函数返回false
。gomaxprocs
是runtime.GOMAXPROCS
的值,表示可以同时运行的最大 goroutine 数量。sched.npidle
是当前系统中空闲的 P(processor)数量,sched.nmspinning
是当前自旋的 M(machine)数量。如果gomaxprocs
小于或等于空闲的 P 数量加上自旋的 M 数量加 1,意味着没有足够的工作需要处理,自旋没有意义,函数返回false
。- 程序可以同时运行的最大 goroutine 数量<=当前有多少个空闲的处理器(P)+当前正在自旋的 goroutine 数量+当前正在尝试自旋的 goroutine(1)
-
1.当有P空闲,可以直接调用P阻塞挂起减少CPU占用
-
2.当P无空闲,自旋大于最大goroutine,无法运行
if p := getg().m.p.ptr(); !runqempty(p) {
return false
}
getg().m.p.ptr()
获取当前 goroutine 运行的 P(processor)的指针。runqempty(p)
检查该 P 的运行队列是否为空。如果不为空,意味着有其他 goroutine 正在等待运行,此时自旋会占用宝贵的 CPU 资源,因此函数返回false
。
sync.Mutex
底层实现图
读写锁(sync.RWMutex)
读写锁在互斥锁的基础上增加了对读取操作的优化。它允许任意数量的读取者(RLock()
)同时访问资源,但写入者(Lock()
)仍然享有独占访问权。sync.RWMutex
提供了RLock()
、RUnlock()
、Lock()
和Unlock()
方法。
sync.RWMutex
是 Go 语言标准库 sync
包中提供的一种读写锁,它用于控制对共享资源的并发访问。与普通的互斥锁(sync.Mutex
)不同,读写锁允许多个读操作同时进行,但在写操作进行时,它会阻止其他读和写操作,确保写操作的独占访问权。这种锁非常适合读多写少的场景,因为它可以提高并发性能。
下面是 sync.RWMutex
提供的方法及其含义:
-
RLock():读锁定。当一个 goroutine 想要读取资源时,它会调用
RLock()
方法来尝试获取锁。如果锁当前没有被写入者持有,那么这个读锁定请求会成功,允许多个读操作同时进行。 -
RUnlock():读解锁。当一个 goroutine 完成对资源的读取后,它会调用
RUnlock()
方法来释放锁。每次RLock()
调用都需要有对应的RUnlock()
调用,以确保锁能够被正确地释放。 -
Lock():写锁定。当一个 goroutine 想要修改资源时,它会调用
Lock()
方法来尝试获取锁。如果锁当前没有被任何读或写操作持有,那么这个写锁定请求会成功,并且这个 goroutine 将独占访问权,直到它调用Unlock()
。 -
Unlock():写解锁。当一个 goroutine 完成对资源的修改后,它会调用
Unlock()
方法来释放锁,允许其他等待的读或写操作获取锁。
sync.RWMutex
的工作原理如下:
- 读锁:如果
sync.RWMutex
的状态表明没有写锁被持有(即没有 goroutine 调用了Lock()
),那么可以安全地增加读锁计数(通常通过增加state
字段的值)。如果有其他 goroutine 已经持有读锁,那么新的读锁请求也可以成功,因为读锁之间不会相互阻塞。 - 写锁:如果
sync.RWMutex
的状态表明没有其他读锁或写锁被持有,那么写锁请求可以成功。一旦写锁被持有,任何新的读锁或写锁请求都将被阻塞,直到写锁被释放。 - 锁释放:当读锁被释放时(通过
RUnlock()
),读锁计数会减少。当最后一个读锁被释放或者写锁被释放时(通过Unlock()
),锁的状态会被更新,以允许等待的读锁或写锁请求获取锁。
使用例子
以下是一个简单的例子,演示如何用 sync.Mutex
保护共享数据(如计数器):
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex // 创建一个互斥锁
counter := 0 // 共享资源
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
mu.Lock() // 加锁
counter++ // 修改共享资源
mu.Unlock() // 解锁
wg.Done()
}()
}
wg.Wait()
fmt.Println("Counter:", counter)
}
解释
mu.Lock()
:在修改counter
之前加锁,确保只有一个 goroutine 能够访问counter
。counter++
:对共享资源进行修改。mu.Unlock()
:完成修改后解锁,让其他 goroutine 可以访问counter
。
这样可以避免多个 goroutine 同时修改 counter
导致的数据错误问题。
下面是没有加锁的代码版本,看看会发生什么情况:
package main
import (
"fmt"
"sync"
)
func main() {
counter := 0 // 共享资源
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
counter++ // 修改共享资源(没有加锁)
wg.Done()
}()
}
wg.Wait()
fmt.Println("Counter:", counter)
}
解释
- 由于没有加锁,多个 goroutine 可能会在同一时间访问
counter
,导致 数据竞争 问题。 - 因为
counter++
操作不是原子的(即,不是一步完成的操作),多个 goroutine 在读取、增加和写入counter
的时候可能会互相干扰。 - 结果是,最终的
counter
值可能小于预期的 5,因为有些增量操作被覆盖或跳过了。
示例输出
运行上面的代码可能会得到各种不同的结果,例如:
Counter: 3
Counter: 4
Counter: 5
因为数据竞争,counter
最终值并不可靠,也不总是预期的结果 5。这就是加锁的必要性,通过加锁可以确保每次只有一个 goroutine 修改 counter
,避免数据竞争带来的不一致。
源码
// 版权所有 2009 Go 语言作者。版权所有。
// 使用此源代码受 BSD 风格
// 许可证的约束,详情见 LICENSE 文件。
// sync 包提供了基本的同步原语,比如互斥锁。除了 Once 和 WaitGroup 类型外,大多数都是供低级库例程使用的。更高级别的同步最好通过通道和通信来完成。
//
// 包含此包中定义的类型的值不应被复制。
package sync
import (
"internal/race"
"sync/atomic"
"unsafe"
)
// 由运行时通过 linkname 提供。
func throw(string)
func fatal(string)
// A Mutex is a mutual exclusion lock.
// The zero value for a Mutex is an unlocked mutex.
//
// A Mutex must not be copied after first use.
//
// In the terminology of the Go memory model,
// the n'th call to Unlock “synchronizes before” the m'th call to Lock
// for any n < m.
// A successful call to TryLock is equivalent to a call to Lock.
// A failed call to TryLock does not establish any “synchronizes before”
// relation at all.
// Mutex 是一个互斥锁。
// Mutex 的零值是一个未锁定的互斥锁。
//
// Mutex 在首次使用后不得被复制。
//
// 在 Go 内存模型的术语中,
// 第 n 次调用 Unlock “在第 m 次调用 Lock 之前同步”
// 对于任何 n < m。
// 成功调用 TryLock 等同于调用 Lock。
// 失败的 TryLock 调用则根本不建立任何 “在...之前同步” 的关系。
type Mutex struct {
state int32 //state用来表示互斥锁的状态
sema uint32 //sema (semaphore)类型的信号量,用于控制等待锁的 goroutine
}
// Locker 表示可以被锁定和解锁的对象。
type Locker interface {
Lock()
Unlock()
}
const (
mutexLocked = 1 << iota // mutex is locked 1开始不是0 mutexLocked 用来表示互斥锁已经被锁定
mutexWoken //mutexWoken,表示互斥锁的等待者已经被唤醒,它的值是 1 << 1(2)。
mutexStarving //mutexStarving互斥锁处于饥饿模式,1 << 2(4)。
mutexWaiterShift = iota //mutexWaiterShift = iota 表示等待者数量的位从哪一位开始。因为这是 const 块的第四个声明,所以 iota 的值是 3,mutexWaiterShift 的值也是 3。这意味着在 state 变量中,等待者数量将从第 3 位开始计数。
// m.state 的状态位分布:
//mutexLocked(位 0):表示互斥锁是否被锁定。
// mutexWoken(位 1):表示互斥锁的等待者是否被唤醒。
// mutexStarving(位 2):表示互斥锁是否处于饥饿模式。
// mutexWaiterShift(位 3 及以后):表示等待者数量的位从第 3 位开始计数。
//下面是一个图像,展示了 m.state 的状态位分布:
// +--------+--------+--------+--------+--------+--------+--------+--------+
// | 31 ... | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
// | ...| waiter| waiter| waiter| mutexWoken | mutexStarving | mutexLocked |
// +--------+--------+--------+--------+--------+--------+--------+--------+
// 状态位示例
// 假设互斥锁被锁定,并且有 5 个 goroutine 在等待(即等待者数量为 5),同时互斥锁处于正常模式(非饥饿模式),m.state 的值可能是:
// +--------+--------+--------+--------+--------+--------+--------+--------+
// | 31 ... | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
// +--------+--------+--------+--------+--------+--------+--------+--------+
// | ... | 0 | 0 | 1 | 0 | 1 | 0 | 1 | 1 |
// +--------+--------+--------+--------+--------+--------+--------+--------+
// Mutex fairness.
//
// Mutex can be in 2 modes of operations: normal and starvation.
// In normal mode waiters are queued in FIFO order, but a woken up waiter
// does not own the mutex and competes with new arriving goroutines over
// the ownership. New arriving goroutines have an advantage -- they are
// already running on CPU and there can be lots of them, so a woken up
// waiter has good chances of losing. In such case it is queued at front
// of the wait queue. If a waiter fails to acquire the mutex for more than 1ms,
// it switches mutex to the starvation mode.
//
// In starvation mode ownership of the mutex is directly handed off from
// the unlocking goroutine to the waiter at the front of the queue.
// New arriving goroutines don't try to acquire the mutex even if it appears
// to be unlocked, and don't try to spin. Instead they queue themselves at
// the tail of the wait queue.
//
// If a waiter receives ownership of the mutex and sees that either
// (1) it is the last waiter in the queue, or (2) it waited for less than 1 ms,
// it switches mutex back to normal operation mode.
//
// Normal mode has considerably better performance as a goroutine can acquire
// a mutex several times in a row even if there are blocked waiters.
// Starvation mode is important to prevent pathological cases of tail latency.
// Mutex 公平性。
//
// Mutex 可以处于两种模式:正常和饥饿。
// 在正常模式下,等待者按 FIFO 顺序排队,但是唤醒的等待者并不拥有互斥锁,而是与新到达的 goroutine 竞争所有权。新到达的 goroutine 有优势——它们已经在 CPU 上运行,并且可能有很多,所以唤醒的等待者有很大的机会失去。在这种情况下,它会排在等待队列的前面。如果一个等待者超过 1ms 未能获得互斥锁,
// 它将切换到饥饿模式。
//
// 在饥饿模式下,互斥锁的所有权直接从解锁的 goroutine 传递给队列前面的等待者。
// 新到达的 goroutine 即使看起来互斥锁未锁定,也不会尝试获取互斥锁,也不会尝试自旋。相反,它们会将自己排在等待队列的末尾。
//
// 如果一个等待者获得了互斥锁的所有权,并看到要么
// (1) 它是队列中的最后一个等待者,或者 (2) 它等待的时间少于 1ms,
//它会首先执行需要锁保护的代码。在执行完这些代码并准备释放互斥锁之前
// 它将互斥锁切换回正常操作模式。
//
// 正常模式的性能明显更好,因为一个 goroutine 可以连续多次获取互斥锁,即使有被阻塞的等待者。
// 饥饿模式对于防止尾部延迟的病理情况很重要。
starvationThresholdNs = 1e6 //定义了饥饿模式的阈值,即如果一个等待者等待的时间超过 1 毫秒(1e6 纳秒),互斥锁将切换到饥饿模式。
)
// Lock 加锁 m。
// 如果锁已经被使用,调用 goroutine
// 会阻塞直到互斥锁可用。
func (m *Mutex) Lock() {
// Fast path: grab unlocked mutex.
// 快速路径:获取未锁定的互斥锁。
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
//如果 CompareAndSwapInt32 返回 true,则表示互斥锁已经被成功锁定,并且如果启用了竞争检测(通过 race 包),则调用 race.Acquire 以记录这次锁的获取。
if race.Enabled {
race.Acquire(unsafe.Pointer(m)) //?
}
return
}
// Slow path (outlined so that the fast path can be inlined)
// 慢速路径(为了使快速路径可以内联而单独列出)
//锁是锁定的通过自旋等待队列等方式加锁
m.lockSlow()
}
// TryLock tries to lock m and reports whether it succeeded.
//
// Note that while correct uses of TryLock do exist, they are rare,
// and use of TryLock is often a sign of a deeper problem
// in a particular use of mutexes.
// TryLock 尝试锁定 m 并报告是否成功。
//
// 注意,尽管 TryLock 的正确用法确实存在,但它们很少见,
// 使用 TryLock 通常是对互斥锁使用方式的更深层次问题的信号。
func (m *Mutex) TryLock() bool {
old := m.state
//检查 state 是否已经被锁定或处于饥饿模式。如果是,则返回 false。
if old&(mutexLocked|mutexStarving) != 0 {
return false
}
// There may be a goroutine waiting for the mutex, but we are
// running now and can try to grab the mutex before that
// goroutine wakes up.
// 可能有一个 goroutine 正在等待互斥锁,但我们正在运行,可以尝试在那个 goroutine 唤醒之前获取互斥锁。
if !atomic.CompareAndSwapInt32(&m.state, old, old|mutexLocked) {
return false
}
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
return true
}
func (m *Mutex) lockSlow() {
var waitStartTime int64
starving := false
awoke := false
iter := 0
old := m.state
for {
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
awoke = true
}
runtime_doSpin()
iter++
old = m.state
continue
}
new := old
if old&mutexStarving == 0 {
new |= mutexLocked
}
if old&(mutexLocked|mutexStarving) != 0 {
new += 1 << mutexWaiterShift
}
if starving && old&mutexLocked != 0 {
new |= mutexStarving
}
if awoke {
if new&mutexWoken == 0 {
throw("sync: inconsistent mutex state")
}
new &^= mutexWoken
}
if atomic.CompareAndSwapInt32(&m.state, old, new) {
if old&(mutexLocked|mutexStarving) == 0 {
break
}
queueLifo := waitStartTime != 0
if waitStartTime == 0 {
waitStartTime = runtime_nanotime()
}
runtime_SemacquireMutex(&m.sema, queueLifo, 1)
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
old = m.state
if old&mutexStarving != 0 {
if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
throw("sync: inconsistent mutex state")
}
delta := int32(mutexLocked - 1<<mutexWaiterShift)
if !starving || old>>mutexWaiterShift == 1 {
delta -= mutexStarving
}
atomic.AddInt32(&m.state, delta)
break
}
awoke = true
iter = 0
} else {
old = m.state
}
}
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
}
func (m *Mutex) Unlock() {
if race.Enabled {
_ = m.state
race.Release(unsafe.Pointer(m))
}
// Fast path: drop lock bit.
// 快速路径:释放锁位。
new := atomic.AddInt32(&m.state, -mutexLocked)
if new != 0 {
m.unlockSlow(new)
}
}
func (m *Mutex) unlockSlow(new int32) {
if (new+mutexLocked)&mutexLocked == 0 {
fatal("sync: unlock of unlocked mutex")
}
if new&mutexStarving == 0 {
old := new
for {
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
return
}
new = (old - 1<<mutexWaiterShift) | mutexWoken
if atomic.CompareAndSwapInt32(&m.state, old, new) {
runtime_Semrelease(&m.sema, false, 1)
return
}
old = m.state
}
} else {
runtime_Semrelease(&m.sema, true, 1)
}
}