在并发编程中,数据共享是一把双刃剑。如果多个协程对同一个资源进行读写而没有任何同步机制,就可能会出现"竞态条件"或"数据竞争"的问题。Go语言为我们提供了
sync.Mutex
,一种最基础也是最常用的加锁方式,用于保证在任意时刻只有一个 goroutine 能访问共享资源。
一、什么是 Mutex?
Mutex
是 mutual exclusion(互斥)的缩写,是 Go 标准库 sync
包提供的一种锁机制。它确保同一时间只有一个 goroutine 能进入临界区访问共享资源。
go
type Mutex struct {
// 内部实现省略
}
常用方法:
- •
Lock()
:获取锁,如果锁已被占用,则阻塞等待; - •
Unlock()
:释放锁,其他阻塞的 goroutine 才能继续执行。
二、为什么需要加锁?
设想一个并发场景:多个 goroutine 同时对一个整数进行自增操作。虽然操作看似简单,但由于 i++
并不是原子操作,它会被分解为三个步骤:
-
- 加载变量值;
-
- 执行加一;
-
- 保存回变量。
在这个过程中,如果没有互斥机制,多个 goroutine 很容易互相干扰,造成结果错误。
三、实战案例:并发安全的计数器
我们将构建一个并发安全的计数器,多个 goroutine 同时对它执行自增操作,确保最终计数正确。
1. 未加锁示例(存在竞态)
go
package main
import (
"fmt"
"sync"
)
var counter int
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
counter++ // 非线程安全
wg.Done()
}()
}
wg.Wait()
fmt.Println("最终计数器结果:", counter)
}
多次运行你会发现,结果每次都不一样,且通常小于1000。说明有些操作被"丢失"了。
2. 使用 sync.Mutex 加锁
go
package main
import (
"fmt"
"sync"
)
var (
counter int
mu sync.Mutex
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
mu.Lock()
counter++ // 临界区
mu.Unlock()
wg.Done()
}()
}
wg.Wait()
fmt.Println("最终计数器结果:", counter)
}
每次运行的结果都是 1000,说明加锁成功避免了竞态条件。
四、延伸:封装一个线程安全的计数器结构
为了更好地管理共享资源,我们可以将计数器封装成一个结构体,并内置锁机制。
go
type SafeCounter struct {
mu sync.Mutex
val int
}
func (s *SafeCounter) Inc() {
s.mu.Lock()
s.val++
s.mu.Unlock()
}
func (s *SafeCounter) Value() int {
s.mu.Lock()
defer s.mu.Unlock()
return s.val
}
使用方式:
go
func main() {
var wg sync.WaitGroup
counter := SafeCounter{}
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
counter.Inc()
wg.Done()
}()
}
wg.Wait()
fmt.Println("最终计数器结果:", counter.Value())
}
五、sync.Mutex 使用建议
✅ 适用场景
- • 多个 goroutine 对同一资源进行读写时;
- • 逻辑简单,读写比例相近的场景。
⚠️ 注意事项
- • 死锁风险:锁住资源后若忘记释放会造成程序阻塞;
- • 性能瓶颈:锁会阻塞其他协程,过多使用可能降低并发效率;
- • 应尽量缩小锁的范围:只包裹必要的临界区。
六、与其他同步机制对比
同步方式 | 特点 |
---|---|
sync.Mutex | 最基础的锁,适用于简单同步控制 |
sync.RWMutex | 读写锁,适用于读多写少的场景 |
channel | 通过通信代替共享数据,较为优雅 |
sync.Atomic 操作 | 支持原子操作,性能比 mutex 高 |
七、结语
通过本案例我们深入理解了 Go 中 sync.Mutex
的使用方式和适用场景。虽然它使用起来非常简单,但如果忽略其潜在的陷阱,可能会导致难以发现的 bug 和性能问题。掌握并合理使用 Mutex
是每一位 Go 开发者进行并发编程的第一步。