1.搞清楚为什么需要用到Mutex?是因为数据竞争吗?什么情况下会出现数据竞争?
- 在多线程情况下,会出现"竞态条件",也就是goroutine无法被控制。
- 如果同一块内存(数据)被多个线程同时访问(至少有一个写操作),就会出现"数据竞争".
- 用Mutex来解决"竞态条件",例如count++并非原子操作,无顺序操作,导致count数量出错。
总结:用Mutex解决数据竞争问题,避免数据错乱。竞态条件是一种状态,数据竞争是一种问题。
2.如何在实战中,应用Mutex锁?
- 清楚哪些情况下,会有数据竞争问题,从而需要用到Mutex锁来避免。
- Mutex使用非常简单:Lock、Unlock、TryLock。
- 谁持有谁释放,如果直接对未加锁的Mutex释放,会导致panic。
- 采用TryLock可以避免goroutine被长时间阻塞,例如限流、降级、快速失败等。
- 操作非原子,Map、Slice、Counter都是。
- 地道写法:
type SafeMap struct {
mu sync.Mutex
m map[string]int
}
func NewSafeMap() *SafeMap {
return &SafeMap{m: make(map[string]int)}
}
func (s *SafeMap) Get(key string) (int, bool) {
s.mu.Lock()
defer s.mu.Unlock()
v, ok := s.m[key]
return v, ok
}
func (s *SafeMap) Set(key string, val int) {
s.mu.Lock()
defer s.mu.Unlock()
s.m[key] = val
}
sm := NewSafeMap()
3.锁的本质?Mutex的历程之路?
- 搞懂CAS,采用CPU的原子指令,Compare And Swap,对同一块内存进行读-写,保证没有竞争时,Lock和UnLock都是在用户态实现的,无需切到内核态。
2. 当某个Goroutine已经持有锁,将当前的Goroutine进行挂起,并且通过信号量去唤醒。
3.数据结构
type Mutex struct {
state int32 // 状态位:Locked/Woken/Starving + 等待者计数
sema uint32 // 信号量,用于阻塞/唤醒
}
4.原子CAS、自旋 -> 公平性与饥饿 ->V1.18 TryLock 非阻塞 -> V1.24 通过自旋优化。
5.V1.24版本自旋优化,提升了70%性能,提高了吞吐
4.Mutex的陷阱有哪些?容易有哪些出错点?
- 写法错了,缺少解锁,建议创建的时候就defer UnLock。
- 重写锁,复制锁,死锁。
- 锁时间过长,导致性能瓶颈。
5.Mutex的扩展
1.因为Map不是线程安全的,高并发情况下:Sync.Map。 2.Go-Zero
import (
"context"
"github.com/zeromicro/go-zero/core/stores/redis"
)
// 1. 构造 Redis 实例
conf := redis.RedisConf{
Host: "127.0.0.1:6379",
Type: "node",
Pass: "",
Tls: false,
}
rds := redis.MustNewRedis(conf)
// 2. 创建锁对象
lock := rds.NewRedisLock("my-redis-lock")
// 3. 可选:调整过期时间(默认 1500ms)
lock.SetExpire(10) // 单位秒
// 4. 获取锁
ok, err := lock.Acquire()
if err != nil {
// 错误处理
}
if !ok {
// 未能拿到锁,直接返回或重试
return
}
defer lock.Release() // 确保释放
// ……临界区:只有当前实例持有锁才会执行到这里