把经典的同步工具总结一下
互斥锁(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(),无论是互斥锁还是读写锁都实现接口中的方法 - 入参是不是指针类型都可以,可以同时多个读锁,但是写锁只能有一个,而且两个锁不能同时开.
除非先开读锁,再开写锁,否则panic - sync.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())
}