Go中经典同步工具

it2022-05-05  118

把经典的同步工具总结一下

互斥锁(sync.Mutex)

加锁:Lock() 解锁:Unlock()全局锁,加锁后未解锁之前再次加锁会panic,解锁依然 入参是不是指针类型都可以 package main import( "fmt" "time" "sync" "math/rand" ) //互斥锁(全局锁)不确定读写时 var lock sync.Mutex func main() { testMap() } func testMap() { var a map[int]int a = make(map[int]int, 5) a[8] = 10 a[3] = 10 a[2] = 10 a[1] = 10 a[18] = 10 for i := 0; i < 2; i++ { go func(b map[int]int) { lock.Lock() b[8] = rand.Intn(100) lock.Unlock() }(a) } lock.Lock() fmt.Println(a) lock.Unlock() time.Sleep(time.Second) fmt.Println(a) }

读写锁(sync.RWMutex)

写锁加锁:sync.Lock 写锁解锁:sync.Unlock读锁加锁:sync.RLock 读锁解锁:sync.RUnlock -返回Locker接口:RLocker Locker接口中有lock()和Unlock(),无论是互斥锁还是读写锁都实现接口中的方法入参是不是指针类型都可以,可以同时多个读锁,但是写锁只能有一个,而且两个锁不能同时开. 除非先开读锁,再开写锁,否则panicsync.WaitGroup必须是指针类型保证唯一 package main import ( "fmt" "sync" ) //读写锁 func main() { var rw sync.RWMutex var wg sync.WaitGroup wg.Add(2) go read(&rw,1,&wg) go read(&rw,2,&wg) wg.Wait() } //wg *sync.WaitGroup必须是地址传递,保证是同一个对象 func read(rw *sync.RWMutex,i int,wg *sync.WaitGroup) { fmt.Println(i,"开始读...") rw.RLock() fmt.Println(i,"正在读...") rw.RUnlock() fmt.Println(i,"结束读...") wg.Done() }

条件变量(sync.Cond)

使用方法:

1 使用cond := sync.NewCond()来创建,入参必须是Locker类型,参考用例2 传递cond时必须是引用,或者取地址,千万不要值传递3 然后加锁,使用wait()等待通知,最后再解锁,wait最好在for循环里面操作.4 使用Signal()和Broadcast()方法发送通知

粗浅原理:

sync.Cond源码结构 type Cond struct { // noCopy可以嵌入到结构中,在第一次使用后不可复制,使用go vet作为检测使用 noCopy noCopy // 根据需求初始化不同的锁,如*Mutex 和 *RWMutex L Locker // 通知列表,调用Wait()方法的goroutine会被放入list中,每次唤醒,从这里取出 notify notifyList // 复制检查,检查cond实例是否被复制 checker copyChecker } 当我们创建了cond的实例以后,调用wait方法,在wait 中会先对cond检查是不是值传递,然后将当前所在goroutine方法notifyList(通知列表中),接着加锁 Wait()函数源码如下: func (c *Cond) Wait() { // 检查c是否是被复制的,如果是就panic c.checker.check() // 将当前goroutine加入等待队列 t := runtime_notifyListAdd(&c.notify) // 解锁 c.L.Unlock() // 等待队列中的所有的goroutine执行等待唤醒操作 runtime_notifyListWait(&c.notify, t) c.L.Lock() } 两个通知函数的源码 func (c *Cond) Signal() { // 检查c是否是被复制的,如果是就panic c.checker.check() // 通知等待列表中的一个 runtime_notifyListNotifyOne(&c.notify) } func (c *Cond) Broadcast() { // 检查c是否是被复制的,如果是就panic c.checker.check() // 唤醒等待队列中所有的goroutine runtime_notifyListNotifyAll(&c.notify) }

疑问 这里摘抄了郝大哥的Go语言核心36讲中的两个问题:

为什么先要锁定条件变量基于的互斥锁,才能调用它的Wait方法?

因为条件变量的Wait方法在阻塞当前的 goroutine 之前,会解锁它基于的互斥锁,所以在调用该Wait方法之前,我们必须先锁定那个互斥锁,否则在调用这个Wait方法时,就会引发一个不可恢复的 panic。 为什么条件变量的Wait方法要这么做呢?你可以想象一下,如果Wait方法在互斥锁已经锁定的情况下,阻塞了当前的 goroutine,那么又由谁来解锁呢?别的 goroutine 吗? 先不说这违背了互斥锁的重要使用原则,即:成对的锁定和解锁,就算别的 goroutine 可以来解锁,那万一解锁重复了怎么办?由此引发的 panic 可是无法恢复的。 如果当前的 goroutine 无法解锁,别的 goroutine 也都不来解锁,那么又由谁来进入临界区,并改变共享资源的状态呢?只要共享资源的状态不变,即使当前的 goroutine 因收到通知而被唤醒,也依然会再次执行这个Wait<方法,并再次被阻塞。 所以说,如果条件变量的Wait方法不先解锁互斥锁的话,那么就只会造成两种后果:不是当前的程序因 panic 而崩溃,就是相关的 goroutine 全面阻塞。

为什么要用for语句来包裹调用其Wait方法的表达式,用if不可以吗?

显然,if语句只会对共享资源的状态检查一次,而for语句却可以做多次检查,直到这个状态改变为止。那为什么要做多次检查呢? 这主要是为了保险起见。如果一个 goroutine 因收到通知而被唤醒,但却发现共享资源的状态,依然不符合它的要求,那么就应该再次调用条件变量的Wait方法,并继续等待下次通知的到来。 这种情况是很有可能发生的

测试例子

package main import ( "fmt" "sync" "time" ) func main() { var locker = new(sync.Mutex) cond := sync.NewCond(locker)//cond最后返回的必须是指针,入参也要是指针 var wg sync.WaitGroup for i:=0;i<40;i++ { wg.Add(1) go func(x int) { cond.L.Lock() defer cond.L.Unlock() cond.Wait() fmt.Println("我等到了通知,我输出:",x) wg.Done() }(i) } time.Sleep(time.Second) fmt.Println("我现在通知其中一个........") cond.Signal() time.Sleep(time.Second) fmt.Println("我再次通知其中一个........") cond.Signal() time.Sleep(time.Second) fmt.Println("我通知所有的..........") cond.Broadcast() wg.Wait() }

原子操作

重点

变量类型从头到尾一致,这也是与锁区别的其中一点 package main import ( "fmt" "sync/atomic" ) func main() { i32 := new(int32) //加法操作 向地址中原值做加法操作,并且返回新值 atomic.AddInt32(i32, 32) addInt := atomic.AddInt32(i32, 2) fmt.Println("addInt : ", addInt) //保存指针的类型做加法操作 uiptr := new(uintptr) uiptrval := atomic.AddUintptr(uiptr, 12) uiptrval = atomic.AddUintptr(uiptr, 23) fmt.Println("uiptrval : ", uiptrval) //减法操作 u开头的类型不能做减法操作 addInt = atomic.AddInt32(i32, -32) fmt.Println("addInt : ", addInt) //CAS 交换值 old值必须和地址中的旧值一样 bool := atomic.CompareAndSwapInt32(i32, 2, 23) fmt.Println("bool : ", bool) fmt.Println("addInt : ", *i32) //获取值 将地址中的值取出来 addInt = atomic.LoadInt32(i32) fmt.Println("addInt : ", addInt) //存值或者更改值 atomic.StoreInt32(i32,100) fmt.Println("StoreInt32 : ", *i32) //存值或者更改值 并把旧值返回 oldInt := atomic.SwapInt32(i32,200) fmt.Println("oldInt : ", oldInt) //原子值 可以放任意类型 但是不能放置nil类型 var atomicValue atomic.Value sli := []string{"1","2","3"} atomicValue.Store(sli)//存储进去,这样存储进去是不安全的,切片是引用传递,可以在外面更改切片的值,可以拷贝一份再放进去 //更改他 sli[0] = "更改他" fmt.Println("已经被更改",atomicValue.Load()) //复制一份 copySlice := make([]string,len(sli)) copy(copySlice,sli) atomicValue.Store(copySlice) //更改他 发现无法被更改回来. sli[0] = "1" fmt.Println(atomicValue.Load()) }

最新回复(0)