锁作用
在 Go 语言中使用 sync.Mutex
进行加锁操作时,锁的作用范围是基于锁实例本身,而不是针对特定函数。
1 核心概念:锁保护的是数据,而不是代码
2 锁作用于互斥量实例
var mu sync.Mutex // 这是一个锁实例
func A() {
mu.Lock()
defer mu.Unlock()
// 临界区代码
}
func B() {
mu.Lock()
defer mu.Unlock()
// 另一个临界区代码
}
在这个例子中:
当
A()
持有了mu
的锁时所有试图在
B()
、A()
或其他函数中锁定同一个mu
的 goroutine 都会被阻塞无论这些函数在哪里被调用
锁保护的是代码执行路径,而不是变量本身。
var a sync.Mutex
var b sync.Mutex
var wg sync.WaitGroup
var s = 1
func A() {
a.Lock()
defer a.Unlock()
defer wg.Done()
s++
time.Sleep(1 * time.Second)
}
func B() {
b.Lock()
defer b.Unlock()
defer wg.Done()
s++
time.Sleep(1 * time.Second)
}
func main() {
wg.Add(2)
go A()
go B()
wg.Wait()
fmt.Println(s)
}
3 锁使用的基本原则
一对一原则:每个共享资源应该有自己专用的锁
// 错误:多个资源共用同一个锁
var lockForEverything sync.Mutex
// 正确:每个资源独立锁定
var (
lockForUsers sync.Mutex
lockForOrders sync.Mutex
)
最小化原则:锁住的范围应尽量小
// 错误:长时间操作锁住资源
func process() {
lock.Lock()
// 长时间计算...
// 网络请求...
lock.Unlock()
}
// 正确:只锁关键操作
func process() {
// 不锁计算操作...
result = heavyCalculation()
lock.Lock()
sharedResource = result // 只锁赋值操作
lock.Unlock()
}
层级原则:如果多个资源有关联,用更高层锁保护
type Account struct {
mu sync.Mutex
balance int
}
type Bank struct {
mu sync.Mutex
accounts map[int]*Account
}
func (b *Bank) Transfer(from, to int, amount int) {
b.mu.Lock() // 顶级锁保护账户映射
defer b.mu.Unlock()
fromAcc := b.accounts[from]
toAcc := b.accounts[to]
fromAcc.mu.Lock()
defer fromAcc.mu.Unlock()
toAcc.mu.Lock()
defer toAcc.mu.Unlock()
fromAcc.balance -= amount
toAcc.balance += amount
}
锁使用情况
在并发编程中,当多个goroutine(协程)需要访问和修改共享资源时,为了防止数据竞争(data race)和保证数据一致性,我们需要使用锁来同步对共享资源的访问。以下情况需要使用锁:
修改共享变量:当多个goroutine会同时修改(写入)同一个变量时。
读写共享变量:当至少有一个goroutine修改共享变量,同时其他goroutine会读取该变量(读操作和写操作同时存在)。
操作非原子性的复合结构:即使操作看起来是读(如读取一个结构体的多个字段),如果这个读操作期间不允许其他goroutine修改,那么也需要锁(或者使用读写锁中的读锁)。
但是,以下情况不需要锁:
只读操作:当所有goroutine都只读取共享变量且没有任何写入操作时,因为只读不会造成数据不一致。
原子操作:某些简单的操作(如整数的自增)可以使用原子操作(sync/atomic包)来避免锁的使用。但是原子操作仅限于简单的整数类型,对于复杂的数据结构还是需要锁。
线程局部存储:如果每个goroutine操作的是自己的数据(即不共享),则不需要锁。
最后更新于