【go基础】13.sync之Map, WaitGroup, Once, Cond, Pool

目录

Map

0、主要思想

1、数据结构

2、特点

3、read map和dirty map

4、接口

(1)Store

(2)Load

(3)Delete

(4)Range

(5)LoadAndDelete

(6)LoadOrStore 复合操作

(7)Swap

(8)CompareAndSwap

(9)CompareAndDelete

5、疑问

WaitGroup

1、数据结构

2、接口

3、特别的

Once

1、数据结构

2、接口 Do(func)

3、特别的

Cond

1、数据结构

2、接口

3、使用场景

Pool

1、数据结构

2、核心思想

3、线程局部存储,Thread-Local Storage,TLS

4、Pool对象的槽和TLS的槽不是同一个概念

5、接口

6、Pool 的生命周期

7、如何保证对象被回收时正确处理


Map


0、主要思想

  • 两个map组成,读写分离,read map仅用于读,dirty map用于存储新的数据
  • 查询操作:read map就像一个高速缓存,查询时先去查read(性能很高);如果没有再去查dirty
  • 写操作:直接写入dirty
  • 读read不需要加锁,写dirty要加锁
  • 有missed记录read被穿透的次数,过多时会将dirty中的数据更新到read中

1、数据结构

type Map struct {
    mu     Mutex
    read   atomic.Value            // 只读字典,内部数据结构也是map[interface{}]*entry
    dirty  map[interface{}]*entry  // 脏字典,原始map,包含已删除的key
    misses int                     // 累加器,从read读key命中失败的次数
}

2、特点

  • sync.Map的零值是有效的,并且零值是一个空的map
  • 它在第一次使用之后,不允许被复制
  • 适用于读多写少的场景

3、read map和dirty map

(1)read map
  • 使用atomic包托管,只读的
  • 主要负责高性能,操作不用加锁
  • read是由dirty升级来的,而不是一点点的set操作出来的
  • read像是一个快照,read中key的集合不能被改变(value可变),所以key的集合可能是不全的
  • amended属性:dirty中是否存在read中没有的数据
(2)dirty map
  • dirty的键值对集合总是全的,但是不包含被擦除(expunged)的
(3)read和dirty的关系
  • 本质上都是map[interface{}]*entry类型(entry可能是具体值,也可能是逻辑删除状态)
(4)read和dirty互转
  • dirty升read
  • 随着load命中失败,miss不断自增,达到阈值后触发
  • dirty置空,miss清零,read.amended=false
  • read转dirty(重塑)
  • 当有read中不存在的新key需要增加,且read和dirty一致时触发
  • 置read.amended=true
  • 把全部entry的nil状态改为expunged
  • 把expunged的entry浅拷贝到dirty中,避免read的key无限的膨胀(存在大量逻辑删除的key)
(5)entry的状态
  • e.p==nil:entry已经被标记删除,不过此时还未经过read=>dirty重塑,此时可能仍然属于dirty(如果dirty非nil)
  • e.p==expunged:entry已经被标记删除,经过read=>dirty重塑,不属于dirty,仅属于read。expunged表示这部分key等待被最终丢弃,下一次dirty升read时,这些expunged的key会被彻底清理(因为升级的操作是直接覆盖,read中的expunged会被自动释放回收)
  • e.p==普通指针:普通状态,属于read;如果dirty非nil,也属于dirty

4、接口

(1)Store

如果read中已有,且不是expunged,直接修改对应值,不用锁

如果read中没有,去dirty中找,如果有(此时dirty中有read没有的key, amended=true)直接修改dirty值(此时read中还是没有)

如果read和dirty中都没有,修改dirty值,且置amended为true(标识dirty中有read没有的key)

(2)Load

在查找指定的key的时候,先去read中寻找,不需要加锁

如果read中没有,且amended=true(dirty中有read没有的key),加锁访问dirty,并记录miss

当misses达到阈值后,dirty会升为read,以提高命中率

(3)Delete

如果key在read中有,将查询到的值置为nil,逻辑删除(后续在重塑时,nil会变成expunged)
如果read中没有,且amended=true,去dirty找,如果有直接delete物理删除,并记录miss

(4)Range

O(N)遍历sync.Map,传入函数返回false时遍历终止

当read包含所有有效元素时,直接遍历read中存储的值;如果dirty含有read中不存在的元素(read.amended),将dirty提升为read,再遍历新read。

即使遍历过程中发生key的并发读写操作,每个key也仅会被最多遍历一次。 

(5)LoadAndDelete

查找key,如果key不存在,返回nil+false;如果存在返回value+true,并将此对象从map中删除

(6)LoadOrStore 复合操作

查找key,如果key存在,返回value+true;如果不存在,将传入值存入map并返回value+false

(7)Swap

类似Store但会返回旧值。查找key,如果key存在,赋新值,返回原始值+true;如果key不存在,赋新值,返回nil+false

(8)CompareAndSwap

如果key的value等于某值,就赋新值;如果不等,直接返回false。

并发环境下,多个协程可能同一个变量进行操作,可能会返回false。如果失败可以通过自旋的方式再次尝试。

(9)CompareAndDelete

如果key的value等于某值,就删除key;否则直接返回false。

5、疑问

- 既然nil也表示标记删除,那么再设计出一个expunged的意义是什么?

expunged是有存在意义的,它作为删除的最终状态(待释放),这样nil就可以作为一种中间状态。如果仅仅使用nil,那么,在read=>dirty重塑的时候,可能会出现如下的情况:
  • 如果nil在read浅拷贝至dirty的时候仍然保留entry的指针(即拷贝完成后,对应键值下read和dirty中都有对应键下entry e的指针,且e.p=nil)那么之后在dirty=>read升级key的时候对应entry的指针仍然会保留。那么最终;的合集会越来越大,存在大量nil的状态,永远无法得到清理的机会。
  • 如果nil在read浅拷贝时不进入dirty,那么之后store某个Key键的时候,可能会出现read和dirty不同步的情况,即此时read中包含dirty不包含的键,那么之后用dirty替换read的时候就会出现数据丢失的问题。
  • 如果nil在read浅拷贝时直接把read中对应键删除(从而避免了不同步的问题),但这又必须对read加锁,违背了read读写不加锁的初衷。
综上,为了保证read作为快照的性质(不能单独删除或新增key),同时要避免Map中nil的key不断膨胀等多个前提要求,才设计成了expungd的状态。 

WaitGroup


1、数据结构

type WaitGroup struct {
        noCopy noCopy     // 保证wg变量不被开发者拷贝。如果把wg赋值给另一个变量会报错
        state1 [3]uint32  // 存储状态和信号量
}

2、接口

  • Add(int):更新计数器counter
  • Wait():counter>0时陷入休眠,归0时被唤醒
  • Done():向Add传入-1

3、特别的

  • wg 必须在 Wait() 方法返回之后才能被重新使用
  • Done() 只是对 Add() 方法的简单封装,我们可以向 Add() 方法传入任意负数(但要保证计数器非负)快速将计数器归零以唤醒等待的 goroutine
  • 可以同时有多个 goroutine 等待当前 wg 计数器的归零,这些 goroutine 会被同时唤醒

Once


保证在 go 程序运行期间的某段代码只会执行一次。

1、数据结构

type Once struct {
        done uint32  // 是否执行过的标识
        m    Mutex   // 锁
}

2、接口 Do(func)

  • 如果函数已执行过,直接返回
  • 如果没有执行过:
  • 先为当前 goroutine 获取互斥锁
  • 执行传入的函数
  • 将done置为1

3、特别的

  • 传入的函数只会被执行一次,哪怕函数中发生了 panic
  • 两次调用 Once.Do 方法传入不同的函数,只会执行第一次调传入的函数

Cond


让一组 goroutine 都在满足特定条件时被唤醒。每一个 Cond  结构体在初始化时都需要传入一个互 斥锁。

1、数据结构

type Cond struct {
        noCopy  noCopy      // 禁止结构体在编译期被拷贝
        checker copyChecker // 禁止结构体在运行期被拷贝
        L       Locker      // 锁
        notify  notifyList  // 要通知的goroutine链表
}

2、接口

  • Wait():将当前goroutine加到notifyList链表末端,并休眠
  • Broadcast():唤醒所有休眠等待的goroutine,按加入队列顺序唤醒
  • Signal():唤醒链表最前面的goroutine

3、使用场景

sync.Cond 不是一个常用的同步机制,但是在条件长时间无法满足时,与使用 for {} 进行忙碌等待相比,Cond 能够让出cpu的使用权,提高 cpu利用率。

与channel相比:channel应用于一收一发的场景,sync.Cond应用于多收一发的场景

Pool


临时对象池,用来保存可以被重复使用的临时对象,避免了反复创建和销毁临时对象带来的消耗以及GC压力。是并发安全的。

1、数据结构

type Pool struct {  
   // 控制不允许被copy
   noCopy noCopy

   // 实际是一个 [P]poolLocal数组
   // 数组大小等于 P 的数量
   local     unsafe.Pointer
   localSize uintptr

   // GC 时,victim 和 victimSize 会分别接管 local 和 localSize
   // victim 的目的是为了减少 GC 后冷启动导致的性能抖动,让分配对象更平滑
   victim     unsafe.Pointer
   victimSize uintptr       

   // 对象初始化构造方法
   New func() interface{}
}

2、核心思想

(1)对象重用

sync.Pool保存了一个由多个槽组成的内部数组,每个槽中保存了一个对象。当一个对象被释放(Put方法)时,会被放入与自身类型对应的槽中,等待被重新使用。这种重用机制避免了频繁的内存分配和垃圾回收,提高了性能。

(2)并发访问控制

使用互斥锁(Mutex)保证并发访问的安全性。

(3)poolLocal本地对象池

每个goroutine都有一个独立的poolLocal,只对该goroutine可见。

当一个goroutine需要一个临时对象时,会先从对应的poolLocal中获取。如果poolLocal中没有可用的对象,会从全局的Pool中获取一个对象,并放入自己的poolLocal中,以便后续重用。

poolLocal的使用可以避免不必要的内存分配和垃圾回收,提高了程序的性能,避免了并发访问和竞争的问题。

3、线程局部存储,Thread-Local Storage,TLS

sync.Pool利用了线程局部变量(TLS)机制。每个goroutine都有自己的TLS槽,用于存储该goroutine特有的临时对象。这样,每个goroutine可以独立地从池中获取和释放对象,无需全局的互斥锁。

当一个goroutine创建临时对象时,该对象会被写入该goroutine的TLS槽中,以便该goroutine后续可以重用该对象。TLS槽的数据写入是自动完成的,无需程序员手动操作。

4、Pool对象的槽和TLS的槽不是同一个概念

Pool对象包含了一组槽,每个槽都对应于一个特定类型的对象。

TLS槽是一个goroutine局部的存储空间(但不是存在栈上而是堆上)。

Pool对象中的槽是共享的,所有goroutine都可以访问和操作,而TLS的槽是每个goroutine独立的,只能被该goroutine访问和操作。

5、接口

// 创建
var pool = sync.Pool{
    New: func() interface{} {
        return “tmp value"
    },
}

// 从池中取出对象 
obj := pool.Get() 
fmt.Println(obj)

// 将对象放回池中 
pool.Put(obj) 

// 再次从池中取出对象 
obj = pool.Get() 
fmt.Println(obj) 

(1)Put() interface{}

把对象放入池子,池子中的对象真正释放的时机是不受外部控制的。

Put将数据放入Pool对象中对应的槽中(和TLS槽无关)。

(2)Get() interface{}

Get时,会首先检查goroutine自己的TLS槽中是否有可用的对象,如果有直接取出使用,再放回槽中;如果没有,会从Pool对象的槽中获取对象。

取对象是随机的,无法保证以固定的顺序获取 Pool 池中存储的对象。

6、Pool 的生命周期

Pool中存储的对象并不会一直存在,由gc控制的,时间是不确定的。

所以要在每次使用之前检查对象是否为空。如果为空需要重新创建对象放入池中。

不能使用sync.Pool存储需要持久化的对象。

7、如何保证对象被回收时正确处理

使用runtimer.SetFinalizer设置回收方法:
func (f *Foo) Close() {
    fmt.Println(“is Closing...")
}


// 注册Finalizer方法
runtime.SetFinalizer(obj, func(f *Foo) {
    f.Close()
})

// gc时,Close方法会被调用
runtime.GC()

  • 15
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值