一只萌萌的囊地鼠(Gopher)在疯狂抢食,结果撞一起了——这就是并发没加锁的写照。
1. 为什么需要锁?地鼠们的粮食争夺战
想象这样一个场景:多个goroutine同时操作同一个共享变量,就像一群地鼠争夺同一个粮仓里的食物。
package main
import (
"fmt"
"sync"
)
var (
count int
wg sync.WaitGroup
)
func add() {
defer wg.Done()
for i := 1; i <= 1000000; i++ {
count++
}
}
func sub() {
defer wg.Done()
for i := 1; i <= 1000000; i++ {
count--
}
}
func main() {
wg.Add(2)
go add()
go sub()
wg.Wait()
fmt.Printf("count = %d\n", count)
}
理论上,这段代码最终应该输出0,因为增加了100万次又减少了100万次。但实际运行结果却出乎意料:每次运行的结果都不一样,而且基本都不是0!
为什么呢?因为count++和count—这类操作并非原子操作,在底层实际上包含了多个步骤:读取值、计算、写入值。当两个goroutine交替执行时,可能会出现这样的情况:
- goroutine A读取count值为100
- goroutine B也读取count值为100
- goroutine A将101写入count
- goroutine B将99写入count
最终结果就成了99,而不是理论上应该的101。这就是数据竞争问题。
2. 互斥锁:粮仓的门牌号
为了解决上述问题,Go提供了互斥锁(Mutex),确保同一时间只有一个goroutine能访问共享资源。
import (
"fmt"
"sync"
)
var (
count int
wg sync.WaitGroup
lock sync.Mutex
)
func add() {
defer wg.Done()
for i := 1; i <= 1000000; i++ {
lock.Lock() // 加锁
count++
lock.Unlock() // 解锁
}
}
func sub() {
defer wg.Done()
for i := 1; i <= 1000000; i++ {
lock.Lock() // 加锁
count--
lock.Unlock() // 解锁
}
}
func main() {
wg.Add(2)
go add()
go sub()
wg.Wait()
fmt.Printf("count = %d\n", count)
}
现在这段代码每次运行都能稳定输出0了!互斥锁就像粮仓的门牌,一次只允许一只地鼠进入,其他地鼠必须排队等待。
2.1 互斥锁的正确使用姿势
使用互斥锁时,有几个关键要点:
// 推荐用法:使用defer保证锁一定会被释放
func updateShared() {
lock.Lock()
defer lock.Unlock() // 无论函数如何退出,都会执行解锁
// 执行共享资源操作
count++
}
// 不推荐用法:手动解锁,可能在异常情况下无法解锁
func updateSharedRisky() {
lock.Lock()
Go并发加锁机制详解

最低0.47元/天 解锁文章

被折叠的 条评论
为什么被折叠?



