go的锁包——sync
上一篇文章介绍了各种锁的基本概念,这篇文章主要学习一下Go的标准库sync包,提供了基本的同步基元.
要注意的是,sync除了Once和WaitGroup类型,大部分都是适用于低水平程序线程,高水平的同步使用channel通信更好一些
互斥锁——Mutex
-
互斥锁: 同一时刻只能有一个读或写的场景
var mu sync.Mutex func main() { mu.Lock() // 使用defer释放锁的话会比显示调用Unlock成本高 defer mu.Unlock() }
-
Mutex实现
// A Mutex is a mutual exclusion lock. // The zero value for a Mutex is an unlocked mutex. // // A Mutex must not be copied after first use. type Mutex struct { state int32 sema uint32 }
-
可以看到Mutex的结构主要是
-
state: 表示锁当前的状态,零值表示未上锁,通过state来进行锁的计数
-
sema: 信号量,实现排队…通过pv操作从等待队列中阻塞/唤醒goroutinue,等待锁的goroutine会挂到等待队列中,并且陷入睡眠不被调度,unlock锁时才唤醒。
-
-
详细的图解解析可以参考这两篇文章:
-
-
一个互斥锁只能同时被一个goroutine锁定,其他go程将被阻塞直到互斥锁被解锁,重新争夺互斥锁
-
要注意的是: Go中没有重入锁, 对一个已经上锁的Mutex再次上锁会导致程序死锁
读写锁——RWMutes
-
读写锁/共享锁: 允许有多个读锁,但只能有一个写锁,读写锁可以分别对读,写进行锁定,一般用于“多读少写”场景
var rw sync.RWMutex func main() { rw.Lock() // 对写操作进行锁定 rw.Unlock() // 对写操作进行解锁 rw.RLock() // 对读操作进行锁定 rw.RUnlock() // 对读操作进行解锁 }
-
写锁权限高于读锁, 写锁会优先锁定,当有写锁时没办法获得读锁, 当只有读锁或无锁时可以获取多个读锁
-
源码解析可以看这篇文章:Golang 读写锁RWMutex
单次执行——Once
-
使用once可以保证只运行一次,是协程安全的
var once sync.Once func main(){ once.Do( func(){ } ) }
-
Once的实现分析:
-
Once结构
// Once is an object that will perform exactly one action. type Once struct { // done indicates whether the action has been performed. // It is first in the struct because it is used in the hot path. // The hot path is inlined at every call site. // Placing done first allows more compact instructions on some architectures (amd64/x86), // and fewer instructions (to calculate offset) on other architectures. done uint32 m Mutex }
- Done : 计数器,统计执行次数
- m: 互斥锁
-
Do
func (o *Once) Do(f func()) { // Note: Here is an incorrect implementation of Do: // // if atomic.CompareAndSwapUint32(&o.done, 0, 1) { // f() // } // // Do guarantees that when it returns, f has finished. // This implementation would not implement that guarantee: // given two simultaneous calls, the winner of the cas would // call f, and the second would return immediately, without // waiting for the first's call to f to complete. // This is why the slow path falls back to a mutex, and why // the atomic.StoreUint32 must be delayed until after f returns. if atomic.LoadUint32(&o.done) == 0 { // Outlined slow-path to allow inlining of the fast-path. o.doSlow(f) } } func (o *Once) doSlow(f func()) { o.m.Lock() defer o.m.Unlock() if o.done == 0 { defer atomic.StoreUint32(&o.done, 1) f() } }
- 当done的次数为0的时候执行doSlow
- 执行后defer将done的值置为1
- 使用原子操作避免加锁提高了性能
-
Once实现单例模式
package singleton import ( "fmt" "sync" ) // 全局实例 type singleton struct { data int } var sing *singleton // 小写私有实例变量 var once sync.Once // 保证线程安全,只执行一次 func GetInstance(num int) *singleton { once.Do(func() { sing = &singleton{data:num} fmt.Println("实例对象的值为和地址为:", sing, &sing) }) return sing }
-
等待组——WaitGroup
-
用于等待一组协程的结束,可以优雅的避免用sleep或for等待.
// 1.创建子协程先调用Add增加等待计数 // 2.子协程结束后调用Done减少协程计数 // 3.主协程中调用Wait方法进行等待,直到计数器归零继续执行 package main import ( "fmt" "sync" "time" ) func main() { var wg sync.WaitGroup wg.Add(1) go func() { time.Sleep(time.Second * 3) // 子协程结束了等待组就调用done wg.Done() fmt.Println("子协程1结束了") }() wg.Wait() fmt.Println("main over") }
条件等待——Cond
-
条件等待是不同协程各用一个锁, 互斥锁是不同协程公用一个锁
-
条件等待不是像互斥锁那样来保护临界区和共享资源的,是用于协调想要访问共享资源的那些线程,维护一个等待队列,在共享资源的状态发生变化时,可以用来通知被互斥锁所阻塞的线程
package main import ( "fmt" "sync" "time" ) /** * 条件等待 */ func main() { var wg sync.WaitGroup cond := sync.NewCond(&sync.Mutex{}) for i := 0; i < 10; i++ { wg.Add(1) go func(i int) { cond.L.Lock() defer cond.L.Unlock() // Wait()等待通知: 阻塞当前线程,直到收到该条件变量发来的通知 cond.Wait() wg.Done() // do other fmt.Println(i) }(i) } fmt.Println("正被阻塞。。。") time.Sleep(time.Second * 1) // Signal()单发通知: 让该条件变量向至少一个正在等待它的通知的线程发送通知,表示共享数据的状态已经改变。 cond.Signal() fmt.Println("通知已被释放") time.Sleep(time.Second * 1) fmt.Println("广播") // Broadcast广播通知: 让条件变量给正在等待它的通知的所有线程都发送通知。 cond.Broadcast() wg.Wait() } // 正被阻塞。。。 // 通知已被释放 // 4 // 广播 // 9 // 3 // 0 // 1 // 2 // 5 // 8 // 7 // 6
协程安全的Map——Sync.Map
-
协程安全的map的特性:
- Sync.Map无须初始化,直接声明即可, 不能使用make创建
- Sync.Map 不能使用 map 的方式进行取值和设置等操作,而是使用 sync.Map 的方法进行调用,Store 表示存储,Load 表示获取,Delete 表示删除。
- Sync.Map的键和值以interface{}类型进行保存
- 使用 Range 配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值,Range 参数中回调函数的返回值在需要继续迭代遍历时,返回 true,终止迭代遍历时,返回 false
- Sync.Map没有获取map数量的方法,可以在Range自行计算
-
package main import ( "fmt" "sync" ) func main() { var scene sync.Map // 将键值对保存到sync.Map scene.Store("key", "value") // 从sync.Map中根据键取值 fmt.Println(scene.Load("key")) // 根据键删除对应的键值对 scene.Delete("key") // 遍历所有sync.Map中的键值对 scene.Range(func(k, v interface{}) bool { fmt.Println("iterate:", k, v) return true }) }
临时对象池——Pool
Go是自动垃圾回收,在高性能场景下,不能任意产生太多垃圾,会造成gc负担重
解决办法: 使用pool来保存和复用临时对象,减少内存分配和gc压力
-
Pool是一个可以分别存取的临时对象的集合,是协程安全的,可以缓存申请但未使用的item用于之后的重用来减少GC的压力
-
Pool不能被复制
- Get: 从池中选择任意一个item,删除池中的引用计数并提供给调用者,如果没有取得item会返回new的结果
- Put: 放入池中
package main import ( "fmt" "runtime" "sync" ) /** * 临时对象池 */ func main() { pool := sync.Pool{New: func() interface{} { return 0 }} pool.Put(1) a := pool.Get() fmt.Println(a) pool.Put(1) runtime.GC() // gc并不会释放池 b := pool.Get() fmt.Println(b) } // 1 // 1
-
为什么不叫cache而叫pool, 更多源码的可以参考go夜读的这个视频