Last updated
Last updated
源码 go\src\runtime\slice.go
根据容量cap*元素size,申请一块内存。mallocgc大空间(大于32kb)才会在heap堆上申请,否则在栈上分配,具体以后再介绍。
这里我们就看到切片底层就是数组,特别的是切片可以增长。
关于增长grow
关于CAP
cap增长策略:
如果期望大于double,新cap就等于期望;
如果当前大小小于1024,则两倍增长;
否则每次增长25%,直到满足期望。
源码 go\src\runtime\hashmap.go
简介
map就是一个hash表。数据被分配到bucket桶数组中。每个桶有8个kv键值对。hash值的低八位用来选择桶。高八位用来在桶内部区分kv。这个很有意思,因为一个指针就是8位。
如果桶中kv超过8个,就新建一个桶,与之相连。
当hash表需要扩容时,我们每次增长两倍的桶数组。桶会从老的桶数组赋值到新的桶数组。
为了维护迭代语义,我们不会移动桶中的key(如果这么做了,key有可能返回0次或者2次)。扩容时,迭代器迭代老表,并且必须检查新表,是否正在迭代的桶已经撤离到了新表。
struct
重要属性:
buckets,桶数组,长度2^B
oldbuckets,老桶数组,扩容时不为nil
nevacuate,num evacuate,撤离进度,小于它的都已经撤离到新桶数组
overflow [2][]*bmap,这个挺有意思的,长度为2的数组,元素是桶数组。overflow 保存溢出的桶,即桶超过8个kv。overflow[0]对应buckets溢出,overflow[1]对应oldbuckets溢出。
桶中只有一个8字节数组,长度为8。tophash保存hash的高8位,根据高8位找到条目。value保存这里就跟大多数map实现不一样,揣测8位就是value的指针?
创建
根据B,创建长度2^B的桶数组。这里B会受到初始值8和load factor平衡因子的影响。
访问
利用hash算法得到key的hash值
这个位运算没看懂,根据注释应该是利用hash的低八位找到bucket
如果oldbuckets不为空,即正在执行扩容,所以优先从oldbuckets读取
根据hash的高8位遍历bucket,得到v。(这一大坨位运算没看懂。)
修改
put过程比较特别,之前说过bucket里面保存的是uint8数组,也就是指针。所以这里需要先给key找到对应的slot,然后将value拷贝到对应的地址。
Background
原子操作即执行过程不能被中断的操作(并发)。
经典问题:i++是不是原子操作?
答案是否,因为i++看上去只有一行,但是背后包括了多个操作:取值,加法,赋值。
加/减
代码很好理解,原子地对i加1。
问题是:为什么加法需要原子性?
思考,在32位OS,操作long类型,是否是原子的?
答案是否,因为32位操作系统CPU一次只能操作32位数据,如果两个64位相加,那么首先会计算低32位,然后计算高32位。
除了操作系统原因,我觉得还有一个原因,因为仅仅i+1并不能对i加1,还必须赋值。
CAS
Compare And Swap,比较并交换,常见的原子操作。
三个入参分别表示:被操作值的指针,被操作值的old值,被操作值的new值。
首先会判断指针指向的被操作值是否与old相等,如果相等就用new替换old,完成数据更新;否则忽略替换操作。整个过程都是原子性的。
Note:CAS与lock区别:
CAS不需要创建互斥量,临界区,所以性能好于lock,这也是性能优化的一个点;
CAS是乐观的,即认为old没有被并发修改,而lock是悲观的,总是认为old是并发不安全的;
如果old被并发修改了,CAS就不会成功。所以我们需要循环多次执行CAS操作,以让他生效,类似自旋。
Load
原子读取变量,防止变量值其他并发读写操作。
还是之前操作系统的例子,在32位操作系统,写入一个long,如果在这个写操作完成前(先改低32位,再改高32位),有一个并发读操作,这个读操作就可能会读取一个只被修改一般的数据。
Store 例子与之前类似。原子存储操作有一个特点,与CAS不同,存储总是成功的,它不关心old。
Swap 与CAS不同,他不关心old;与Store不同,它会返回old。介于两者之间。
原子值 上面的原子操作都只支持基本类型,如果想对其他类型进行原子操作怎么办?所以Go提供了原子值类型,它对于类型没有限制。它支持Load和Store方法。
执行Store之后,Value禁止copy。目前已知的copy行为包括:赋值其他变量,函数入参,函数返回值,传入chan。
store不允许nil,并且类型必须与第一次store一致,否则panic。
sync.Map是1.9才推荐的并发安全的map,除了互斥量以外,还运用了原子操作,所以在这之前,有必要了解下Go语言——原子操作
源码 go1.10\src\sync\map.go
Struct
read: readOnly, 保存了map中可以并发读的数据。并发写有可能不需要互斥,但是更新之前标记删除的数据(从一个value指针变成expunged指针),就需要将条目拷贝到dirty中,并且unexpunged操作(?还不懂什么意思,将expunged指针变成value指针?)需要锁。
dirty: 脏数据需要锁。为了尽快提升为read,dirty需要保存read中所有未标记删除的数据(减少数据拷贝?)。标记删除的条目不保存的dirty里面(那就是保存在read里面咯)
misses: 从read里面读,miss之后再从dirty里面读。当miss多次之后,就将dirty提升为read。
entry分为三种情况:
nil:已经被删除,并且dirty不存在
expunged:已经被删除了,dirty存在,且条目不在dirty里面,标记删除
其他情况:保存在read和dirty(存在)中
Store
从read中读取key,如果key存在就tryStore。
原子地读取条目的值
如果条目被标记删除了(空接口指针),返回false;按照之前的逻辑,就是说如果条目被标记了,就继续store
尝试CAS新的value值,成功代表更新条目值成功。
CAS失败,重新原子load,继续CAS,除非发现条目被其他的G标记了。
```
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
}
func (e *entry) unexpungeLocked() (wasExpunged bool) { return atomic.CompareAndSwapPointer(&e.p, expunged, nil) }
func (e entry) storeLocked(i interface{}) { atomic.StorePointer(&e.p, unsafe.Pointer(i)) }
else if e, ok := m.dirty[key]; ok { e.storeLocked(&value) } else { if !read.amended { m.dirtyLocked() m.read.Store(readOnly{m: read.m, amended: true}) } m.dirty[key] = newEntry(value) } m.mu.Unlock() }
func (m *Map) Load(key interface{}) (value interface{}, ok bool) { read, := m.read.Load().(readOnly) e, ok := read.m[key] if !ok && read.amended { m.mu.Lock() // Avoid reporting a spurious miss if m.dirty got promoted while we were // blocked on m.mu. (If further loads of the same key will not miss, it's // not worth copying the dirty map for this key.) read, = m.read.Load().(readOnly) e, ok = read.m[key] if !ok && read.amended { e, ok = m.dirty[key] // Regardless of whether the entry was present, record a miss: this key // will take the slow path until the dirty map is promoted to the read // map. m.missLocked() } m.mu.Unlock() } if !ok { return nil, false } return e.load() }
func (m *Map) missLocked() { m.misses++ if m.misses < len(m.dirty) { return } m.read.Store(readOnly{m: m.dirty}) m.dirty = nil m.misses = 0 }
func (m *Map) Delete(key interface{}) { read, := m.read.Load().(readOnly) e, ok := read.m[key] if !ok && read.amended { m.mu.Lock() read, = m.read.Load().(readOnly) e, ok = read.m[key] if !ok && read.amended { delete(m.dirty, key) } m.mu.Unlock() } if ok { e.delete() } }
func (e *entry) delete() (hadValue bool) { for { p := atomic.LoadPointer(&e.p) if p == nil || p == expunged { return false } if atomic.CompareAndSwapPointer(&e.p, p, nil) { return true } } }
func (e *entry) tryExpungeLocked() (isExpunged bool) { p := atomic.LoadPointer(&e.p) for p == nil { if atomic.CompareAndSwapPointer(&e.p, nil, expunged) { return true } p = atomic.LoadPointer(&e.p) } return p == expunged }
func (m *Map) dirtyLocked() { if m.dirty != nil { return }
}
func (m *Map) missLocked() { m.misses++ if m.misses < len(m.dirty) { return } m.read.Store(readOnly{m: m.dirty}) m.dirty = nil m.misses = 0 }
``` 这里其实就跟miss逻辑串起来了,因为miss达到阈值之后,dirty会全量变成read,也就是说标记删除在这一步最终删除。这个还是很巧妙的。
真正的删除逻辑:
delete将条目变成nil
store的时候,将nil的条目标记,并且这些条目不会存在于dirty中
load的时候,如果miss达到阈值,就将dirty全量变成read,变现地删除了nil条目