Golang 锁

hashmap 和 sync.Map 皆为 unscalable:并发执行 hashmap 的插入操作,因为锁的存在,使用越多cpu,平均操作耗时越长。用 sync.Map 比用 hashmap+锁 的平均操作耗时更长。

一、数据竞争

原因:多个 goroutine 同时接触一个变量,行为不可预知
认定条件:两个及以上 goroutine 同时接触一个变量,其中至少一个 goroutine 为写操作
检测方案:go run -race 或者 go test -race

二、锁的最佳实践

  1. 减少持有时间:使用 defer 释放锁的时候注意不要增加临界区(尽量用完了就尽快解锁,而defer会在函数最后才被执行,虽然不会忘记解锁,但是加大了持有时间)
  2. 优化锁的粒度:空间换时间。map 分段锁,比如一个数组长度为x,可以创建一个长度为x的 lock 数组分别对每个位置元素加锁,利用下标控制对应的锁。
  3. 读写分离:尽量使用读写锁 RWMutex,不管读多写少或者读少写多的场景,相比于 sync.Mutex 仍然会有不少的性能提升;sync.Map 相比于 RWMutex 在 cpu 核数增加时性能更稳定,也是推荐使用。
  4. 使用原子操作(Lock Free):使用 Go 提供的 atomic.Value。不触发调度,不阻塞执行流。相当于没有命中缓存的访存指令。atomic.Value 的前世今生

三、避免踩坑

  1. 不要拷贝mutex(传参时要传指针,Goland 已默认提示)
  2. 避免死锁,Go 的 sync.Mutex 是不可重入锁,不可以重复 lock。Go Mutex 设计思想
  3. atomic.Value 中应存入只读对象,如果需要取出来再写,则需要加上 Mutex 锁住该操作。
  4. race detector 发现潜在问题(用于单测,压测,但是会 10 倍 cpu,不要在线上环境开启)

四、锁的进化

  1. Mutex 设计原则:效率优先,兼顾公平
  2. 正常模式:效率高,新来的goroutine直接先抢锁,无需先排队,等待超过1ms,则切换到饥饿模式 。 为什么正常模式效率高:减少调度开销,新来的不用进入队列;可以充分利用缓存
  3. 饥饿模式:更公平,新来的goroutine也要先排队,完全先来后到。
  4. 加锁:原子操作 -> 自旋 -> 加入队列
  5. 唤醒:不是饥饿状态,可能和其他goroutine竞争;是饥饿状态,直接获得锁,更新锁的状态
  6. 解锁:fast_path;slow_path

五、引用

  1. Go 不安全的 string
  2. Go: 关于锁的1234
  3. Go中锁的那些姿势

你可能感兴趣的:(golang,并发,并发编程)