Go Sync包
sync.Map
- sync.Map主要针对于Map对于并发读写不支持的场景下提出实现的,其原理是通过对map的写操作进行加锁:Sync.RWMutex
- 同时sync.Map实现了读写分离,当对map进行读操作时,通过读read Map, 当read Map中不存在是去dirty map中读取
sync.Map的核心数据结构如下:
type Map struct {
me Mutex
read atomic.Value // readOnly,读数据
dirty map[interface{}]*entry // 包含最新的写入数据,当missed达到一定的值时,将值赋给read
misses int // 计数作用,每次从read中读失败,则missed加一
}
// readOnly的数据结构
type readOnly struct{
m map[interface{}]*entry
amended bool // Map.dirty中的数据和这里的m中的数据不同时值为true
}
// entry的数据结构:
type entry struct {
p unsafe.Pointer // *interface{}
// 可见value是一个指针值,虽然read和dirty存在冗余情况,但由于是指针类型,存储空间不会太多
}
sync.Map相关问题
- sync.Map的核心实现:两个map,一个用于写,一个用于读,这样的设计思想可以类比于缓存与数据库
- sync.Map的局限性:如果写远高于读,dirty -> readOnly这个类似于刷新数据的频率较高,不如直接使用mutex + map的效率高
- sync.Map的设计思想:保证高频率读的无锁结构,空间换时间的思想
sync.Map包的简单使用:
func main(){
var m sync.Map
m.Store("a", 1)
fmt.Println(m.Load("a"))
// Tip: LoadOrStore存在就加载数据,没有就保存数据
fmt.Println(m.LoadOrStore("a", 2))
m.Delete("a")
fmt.Println(m.LoadOrStore("a", 3))
fmt.Println(m.LoadOrStore("b", 4))
// 迭代map
m.Range(func (key, value interface{}) bool {
fmt.Println(key, value)
return true
})
}
sync.Cond
- sync.Cond实现了在Locker的基础上进行消息广播和单播的功能。其内部维护一个等待队列,队列中存放的是所有等待sync.Cond的goroutine,及保存了
一个通知队列。sync.Cond可以用来唤醒一个或所有因等待条件而阻塞的goroutine,以此来实现以此来实现多个goroutine之间的同步.
sync.Cond的结构体如下:
type Cond struct {
noCopy npCopy // noCopy嵌入到结构体中,第一次使用之后不可复制,使用go vet作为检测使用
L Locker // 根据需求初始化不同的锁,如*Mutex, *RWMutex
notify notifyList // 通知列表,调用wait()方法的goroutine会被放入到list中,每次唤醒从这里取出
checker copyChecker //复制检查,检查cond实例是否被复制
}
wait函数实现:必须在当前协程获取锁之后才能Wait,Wait方法会在调用时先释放底层锁Locker,并且将当前的goroutine挂起,直到
另一个goroutine执行Signal或者Broadcast,该goroutine才有机会重新被唤醒,并尝试获取Locker
func (c *Cond) Wait(){
// 检查cond是否是被复制的,如果是就panic掉
c.checker.Check()
// 将当前goroutine加入等待队列
t := runtime_notifyListAdd(&c.notify)
// 解锁
c.L.UnLock()
// 等待队列中所有的goroutine执行唤醒
runtime_notifyListWait(&c.notify,t)
c.L.Lock()
}
Signal函数:任意唤醒队列中的一个goroutine
func (c *Cond) Signal(){
// 检查cond是否是被复制的,如果是就panic掉
c.checker.Check()
//通知等待队列中的一个goroutine被唤醒
runtime_notifyListNotifyOne(&c.notify)
}
Broadcast函数:唤起等待队列中所有的goroutine
func (c *Cond) Broadcast(){
// 检查cond是否是被复制的,如果是就panic掉
c.checker.Check()
runtime_notifyListNotifyAll(&c.notify)
}
sync.Cond的相关问题
- sync.Cond的核心实现:通过一个锁,封装了notify通知的实现,包括了单个通知和广播两种方式
- sync.Cond与Channel的异同:channel应用于一收一发的场景,sync.Cond应用于多收一发的场景
sync.Cond的简单使用示例:
func main(){
var m sync.Mutex
c := sync.NewCond(&m)
// Tip: 主协程先获得锁
c.L.Lock()
go func(){
// Tip: 协程一开始无法获得锁
c.L.Lock()
defer c.L.Unlock()
fmt.Println("协程获得锁")
time.Sleep(time.Second)
// Tip: 通过notify进行广播通知
c.Broadcast()
fmt.Println("协程执行完毕,即将执行defer中的解锁操作")
}()
fmt.Println("主协程获得锁")
time.Sleep(time.Second)
fmt.Println("主协程即将释放掉锁,此时仍然占有")
// Tip: Wait的实现,先释放锁,直到收到了notify,又进行加锁
c.Wait()
c.L.Unlock()
fmt.Println("Done")
}
Sync.Pool
- 由于Golang GC对频繁创建取消对象的性能影响,及对于使用频繁但使用周期不长的对象,为了减少GC,golang提供了对象重用机制,
也就是sync.Pool对象池。sync.Pool是可以伸缩的,并发安全的。其大小仅仅受限于内存大小,可以被看作是一个存放重用对象的容器。目的是
为了存放已经分配但是暂时不用的对象,在需要的时候直接从pool中取出
sync.Pool的底层数据结构:
type Pool struct {
noCopy noCopy
local unsafe.Pointer // 固定大小的per-P池,实际类型为[P]poolLocal
localSize uintprt // local array的大小
New func()interface{} // New方法在Get失败的情况下,选择性的创建一个只,否则返回nil
}
type PoolLocal struct {
poolLocalInternal
// 将poolLocal补齐至两个缓存行的倍数,防止false sharing(伪共享)
// 每个缓存行具有64bytes, 及512bit
pad [128 - unsafe.Sizeof(poolLocalInternal{}) % 128]byte
}
type poolLocalInternal struct {
private interface{} // 只能被局部调度器P使用
shared []interface{} // 所有P共享
Mutex // 访问共享数据域的锁
}
- 为了使在多个goroutine中高效的使用对象,sync.Pool为每个P(process)都分配一个localPool,当执行Get或者Put的时候,会先将goroutine和某个
P的localPool相关联,再对该子池进行操作。每个P的子池分为私有对象和共享列表对象,私有对象只能被特定的P访问,共享列表对象可以被任何P访问。因为同一时刻一个P只能
执行一个goroutine,所以无需加锁,但对共享列表对象进行操作时,则需要加锁。
Put函数实现:Put的过程就是将零时对象放进pool:
- 如果放入的值为nil直接返回
- 检查当前goroutine是否设置对象私有池的值,如果没有则将x赋值给其私有成员,并将x设置为nil
- 如果当前goroutine私有值已经被设置,那么将该值追加到共享列表
func (p *Pool)Put(x interface{}){
if x == nil {
return
}
// 获取localPool
l := p.pin()
// 优先放入private
if l.private == nil {
l.private = x
return
}
runtime_procUnpin()
// 如果不能放入private,则放入shared
if x != nil {
l.Lock()
l.shared = append(l.shared, x)
l.Unlock()
}
}
Get函数实现:在从池中获取对象的时候,会先从per-P的poolLocal slice中选取一个poolLocal
- 尝试从本地P对应的那个本地池中获取一个对象值,并从本地池中删除该值
- 如果获取失败,那么从共享池中获取,并从共享池中删除该值
- 如果获取失败,那么从其他P的共享池中获取,并删除共享池中的该值
- 如果仍然失败,那么直接通过New()分配一个返回值,注意这个分配的值不会被放入到池中。New()返回用户注册的New()函数的值,如果用户未注册,那么返回nil
func (p *Pool) Get() interface{} {
// 现获取poolLocal
l := p.pin()
// 先从private中去取
x := l.private
l.private = nil
runtime_procUnpin()
// private不存在再从shared中去取
if x == nil {
// 加锁,从shared中获取
l.Lock()
// 从shared尾部取缓存对象
last := len(l.shared) - 1
if last >= 0 {
x = l.shared[last]
l.shared = l.shared[:last]
}
l.Unlock()
if == nil {
// 如果取不到,则获取新的缓存对象,从其他的P共享池中获取
x = p.getSlow()
}
}
// 如果getSlow还是获取不到,则New一个
if x == nil && p.New = nil {
x = p.New()
}
return x
}
- 在go 1.3版本之后,取消了mutex的实现,该用了poolChain实现SPMC无锁队列,避免了锁的开销,mutex变成了atomic. 同时新增了victim cache,其目的是
在GC处理过程中直接回收oldPools的对象,GC处理是并不直接将AllPools的对象直接进行GC,而是保存到oldPools,等到下一次GC周期到了再处理
这样的优势是增加了Get获取内存的选项,增加了对象复用的概率,时间上通过延迟GC,增加对象复用的时间长度。
Sync.Pool相关问题:
- sync.Pool的核心作用:缓存稍后会频繁使用的对象 + 减轻GC压力
- sync.Pool的Put与Get: Put的顺序为local private -> local shared, Get的顺序为local private -> local shared -> remote shared
- 思考sync.Pool应用的核心场景:高频使用且生命周期短的对象,且初始化始终一致,如fmt
- 1.3中引入victim的作用: victim cache机制
sync.Pool的简单使用
func main(){
var sp = sync.Pool{
// Tip: 声明对象池的New函数,这里以一个简单的int为例
New: func() interface{} {
return 100
},
}
// Tip: 从对象池中获取一个对象
data := sp.Get().(int)
fmt.Println(data)
// Tip: 往对象池中放回一个对象
sp.Put(data)
}
Sync.Once
- sync.Once被用于控制变量的初始化,这个变量的读写通常遵循单例模式:
- 当且仅当第一次读某个变量时进行初始化
- 变量被初始化的过程中,所有的读都被阻塞,当变量初始化写完之后,读操作继续
- 变量仅初始化一次,初始化后驻留在内存中
- sync.Once作用与init函数类似,不同之处在于:
- init函数在文件包首次被家在的时候执行,且只执行一次
- sync.Once是在代码运行中需要的时候执行,且只执行一次
sync.Once的底层数据结构:sync.Once使用变量done来记录函数的执行状态,使用sync.Mutex和sync.atomic来保证线程安全的读取done
type Once {
m Mutex
done uint32 // 状态位:判断变量是否初始化完成,有效值为0, 1
}
Do函数实现:
func (o *Once) Do(f func()){
if atomic.LoadUint32(&o.done) == 1 {
return
}
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUnit32(&o.done, 1)
f()
}
}
sync.Once的简单示例
func main(){
var once sync.Once
onceBody := func(){
fmt.Println("Only Once")
}
done := make(chan bool)
for i := 0; i < 10; i++{
go func(){
// Tip: 调用Do方法只执行一次onceBody方法
once.Do(onceBody)
done <- true
}()
}
for i := 0; i < 10; i++{
<-done
}
}
sync.WaitGroup
- sync.WaitGroup常用于针对goroutine的并发执行,通过WaitGroup可以等待所有的go程序执行结束之后再执行之后的逻辑
- WaitGroup对象内部有一个计数器,最初重0开始,提供了三个方法:Add(),Done(),Wait()用来控制计数器的数量。Add(n)把计数器设置为n,
Done()每次把计数器减一,Wait()会阻塞代码的执行,直到计数器的值减到0为止。
Add函数实现:
func (wg *WaitGroup) Add(delta int){
statep := wg.state()
// 更新statep,state将在wait和add中通过原子操作一起使用
state := atomic.AddUint64(statep, uint64(delta)<<32)
v := int32(state >> 32)
w := uint32(state)
if v < 0 {
panic("sync: negative WaitGroup Counter")
}
if w != 0 && delta > 0 && v == int32(delta) {
// wait不等于0说明已经执行了wait,此时不允许Add()
panic("sync: WaitGroup misuse: Add Called concurrently with Wait")
}
// 正常情况下,Add会让v增加,Done会让v减少,如果没有全部Done掉,此时v总是会大于0的,知道v等于0才往下走
// 而w代表有多少个goroutine在等待信号,wait中通过compareAndSwap对这个w进行加一
if v > 0 || w == 0{
return
}
// 当v为0或者w不为0才会到这里,但是这个过程中又有一次Add,导致panic
if *statep != state {
panic("sync: WaitGroup misuse: Add Called concurrently with Wait")
}
*statep = 0
// 将信号发出,出发wait()结束
for ; w != 0; w-- {
runtime_Semrelease(&wg.sema, false)
}
}
Done函数实现:
func (wg *WaitGroup) Done(){
wg.Add(-1)
}
Wait函数实现:
func (wg *WaitGroup) Wait(){
statep := wg.state()
for {
state := atomic.LoadUint64(statep)
v := int32(state >> 32)
w := uint32(state)
if v == 0 {
if race.Enabled {
race.Enable()
race.Acquire(unsafe.Pointer(wg))
}
return
}
// 如果statep和state相等,则增加等待计数,同时进入if等待信号量
// 此处做CAS, 主要是防止多个goroutine里进行Wait()操作,每又一个goroutine进行Wait,等待计数就加一
// 这里如果不想等,说明statep在从读出来到CAS比较的时间内,被别的goroutine改写了,那么就不进入if,回去在读一次,这样避免使用锁,更高效
if atomic.CompareAndSwapUint64(statep, state, state + 1){
if race.Enabled && w == 0 {
race.Write(unsafe.Pointer(&wg.sema))
}
// 等待信号量
runtime_Semacquire(&wg.sema)
// 信号量到来,代表所有的Add都Done了
if *statep != 0 {
panic("sync: WaitGroup is reused before previous Wait has returned")
}
return
}
}
}
sync.WaitGroup的简单使用
func main(){
wg := sync.WaitGroup{}
// Tip: 计数器加100
wg.Add(100)
for i := 0; i < 100; i++{
go func(i int){
fmt.Println("这是第" + strconv.Itoa(i) + "个")
// Tip: 每次Done -1
wg.Done()
}(i)
}
wg.Wait()
}