目录
3、线程局部存储,Thread-Local Storage,TLS
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
-
使用atomic包托管,只读的
-
主要负责高性能,操作不用加锁
-
read是由dirty升级来的,而不是一点点的set操作出来的
-
read像是一个快照,read中key的集合不能被改变(value可变),所以key的集合可能是不全的
-
amended属性:dirty中是否存在read中没有的数据
-
dirty的键值对集合总是全的,但是不包含被擦除(expunged)的
-
本质上都是map[interface{}]*entry类型(entry可能是具体值,也可能是逻辑删除状态)
-
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)
-
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
(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的意义是什么?
-
如果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读写不加锁的初衷。
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
1、数据结构
type Once struct {
done uint32 // 是否执行过的标识
m Mutex // 锁
}
2、接口 Do(func)
-
如果函数已执行过,直接返回
-
如果没有执行过:
-
先为当前 goroutine 获取互斥锁
-
执行传入的函数
-
将done置为1
3、特别的
-
传入的函数只会被执行一次,哪怕函数中发生了 panic
-
两次调用 Once.Do 方法传入不同的函数,只会执行第一次调传入的函数
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
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、如何保证对象被回收时正确处理
func (f *Foo) Close() {
fmt.Println(“is Closing...")
}
// 注册Finalizer方法
runtime.SetFinalizer(obj, func(f *Foo) {
f.Close()
})
// gc时,Close方法会被调用
runtime.GC()