Golang 互斥锁Mutex的深入理解与应用

48 阅读2分钟

1.搞清楚为什么需要用到Mutex?是因为数据竞争吗?什么情况下会出现数据竞争?

  1. 在多线程情况下,会出现"竞态条件",也就是goroutine无法被控制。
  2. 如果同一块内存(数据)被多个线程同时访问(至少有一个写操作),就会出现"数据竞争".
  3. 用Mutex来解决"竞态条件",例如count++并非原子操作,无顺序操作,导致count数量出错。

总结:用Mutex解决数据竞争问题,避免数据错乱。竞态条件是一种状态,数据竞争是一种问题。

2.如何在实战中,应用Mutex锁?

  1. 清楚哪些情况下,会有数据竞争问题,从而需要用到Mutex锁来避免。
  2. Mutex使用非常简单:Lock、Unlock、TryLock。
  3. 谁持有谁释放,如果直接对未加锁的Mutex释放,会导致panic
  4. 采用TryLock可以避免goroutine被长时间阻塞,例如限流、降级、快速失败等。
  5. 操作非原子,Map、Slice、Counter都是。
  6. 地道写法:
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的历程之路?

  1. 搞懂CAS,采用CPU的原子指令,Compare And Swap,对同一块内存进行读-写,保证没有竞争时,Lock和UnLock都是在用户态实现的,无需切到内核态。

image.png 2. 当某个Goroutine已经持有锁,将当前的Goroutine进行挂起,并且通过信号量去唤醒。

3.数据结构

type Mutex struct {
    state int32   // 状态位:Locked/Woken/Starving + 等待者计数
    sema  uint32  // 信号量,用于阻塞/唤醒
}

image.png

4.原子CAS、自旋 -> 公平性与饥饿 ->V1.18 TryLock 非阻塞 -> V1.24 通过自旋优化。

5.V1.24版本自旋优化,提升了70%性能,提高了吞吐

image.png

4.Mutex的陷阱有哪些?容易有哪些出错点?

  1. 写法错了,缺少解锁,建议创建的时候就defer UnLock。
  2. 重写锁,复制锁,死锁。
  3. 锁时间过长,导致性能瓶颈。

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() // 确保释放

// ……临界区:只有当前实例持有锁才会执行到这里