TheRiver | blog

You have reached the world's edge, none but devils play past here

0%

golang sync map

struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Map is like a Go map[interface{}]interface{} but is safe for concurrent use
// by multiple goroutines without additional locking or coordination.
// Loads, stores, and deletes run in amortized constant time.
//
// The Map type is specialized. Most code should use a plain Go map instead,
// with separate locking or coordination, for better type safety and to make it
// easier to maintain other invariants along with the map content.
//
// The Map type is optimized for two common use cases: (1) when the entry for a given
// key is only ever written once but read many times, as in caches that only grow,
// or (2) when multiple goroutines read, write, and overwrite entries for disjoint
// sets of keys. In these two cases, use of a Map may significantly reduce lock
// contention compared to a Go map paired with a separate Mutex or RWMutex.
//
// The zero Map is empty and ready for use. A Map must not be copied after first use.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
type Map struct {
mu Mutex

// read contains the portion of the map's contents that are safe for
// concurrent access (with or without mu held).
//
// The read field itself is always safe to load, but must only be stored with
// mu held.
//
// Entries stored in read may be updated concurrently without mu, but updating
// a previously-expunged entry requires that the entry be copied to the dirty
// map and unexpunged with mu held.
read atomic.Value // readOnly

// dirty contains the portion of the map's contents that require mu to be
// held. To ensure that the dirty map can be promoted to the read map quickly,
// it also includes all of the non-expunged entries in the read map.
//
// Expunged entries are not stored in the dirty map. An expunged entry in the
// clean map must be unexpunged and added to the dirty map before a new value
// can be stored to it.
//
// If the dirty map is nil, the next write to the map will initialize it by
// making a shallow copy of the clean map, omitting stale entries.
dirty map[interface{}]*entry

// misses counts the number of loads since the read map was last updated that
// needed to lock mu to determine whether the key was present.
//
// Once enough misses have occurred to cover the cost of copying the dirty
// map, the dirty map will be promoted to the read map (in the unamended
// state) and the next store to the map will make a new dirty copy.
misses int
}


map

Load()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

// Load returns the value stored in the map for a key, or nil if no
// value is present.
// The ok result indicates whether value was found in the map.
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()
}

note

  • 先从read查,查到就直接返回。查不到并且read.amended=true(dirty!=read)再去dirty中查,查dirty要加互斥锁。
  • 这里用到了两次查询,参考单例的设计。
  • read是atomic.value类型的,atomic.value的Load/Store是线程安全的,所以不用加锁,读性能比较好。但是其实也用到了cas,所以也用到了lock指令,不是完全无锁定的。

demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"fmt"
"sync"
"sync/atomic"
)

type Map struct {
mu sync.Mutex
read atomic.Value // readOnly
dirty map[interface{}]*int
misses int
}

type readOnly struct {
m map[interface{}]*int
amended bool // true if the dirty map contains some key not in m.
}

func main() {
m := Map{}
//fmt.Println(m.read.Load().(readOnly)) //panic
fmt.Println(m.read.Load()) //<nil>

var v interface{}
read, _ := (v).(readOnly)
fmt.Println(read) //{map[] false}
}

Store()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

// Store sets the value for a key.
func (m *Map) Store(key, value interface{}) {
read, _ := m.read.Load().(readOnly)
//先从read读,读成功再尝试store更新
//tryStore先load,如果标记为expunged则ret false, ow store
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}

//read读不到则store到dirty,要加锁
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
//两次读,第2次读到了
if e, ok := read.m[key]; ok {
//unexpungeLocked:如果e.p=expunged,则set e.p=nil
if e.unexpungeLocked() {
// The entry was previously expunged, which implies that there is a
// non-nil dirty map and this entry is not in it.
m.dirty[key] = e
}
//atomic.StorePointer 更新entry
e.storeLocked(&value)
//dirty中有,则更新
} else if e, ok := m.dirty[key]; ok {
e.storeLocked(&value)
//read/dirty都没有
//新增数据
} else {
//并且read.amended=0
if !read.amended {
// We're adding the first new key to the dirty map.
// Make sure it is allocated and mark the read-only map as incomplete.
//如果dirty=nil,则把read(未删除的)拷贝给dirty
//初始化后第一次store会进入这里,或者misscount>=len(dirty)也会进来重新刷新dirty.
m.dirtyLocked()
//更新了amended
m.read.Store(readOnly{m: read.m, amended: true})
}
//新增entry并加到dirty
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
}

note

  • 任何值可以赋值给空接口的值,空接口的值也可以赋值给任何值
  • 第一次新增和misscount达到限制都会刷新dirty(吧read赋值给dirty,如果read为空,这一步就提前ret)
  • store包含了更新和新增
  • 这里加锁是给m.dirty加锁,加锁是内核的调度器执行的主要用于大的复合对象/临界区,原子操作是硬件指令主要针对单个值
  • (新增元素或者刷新dirty的时候)tryExpungeLocked将nil的标记为已删除expunged,新增entry的时候read如果能读到,unexpungeLocked对于expunged又改为nil并更新dirty

demo

missLocked

1
2
3
4
5
6
7
8
9
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
}

LoadOrStore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

// LoadOrStore returns the existing value for the key if present.
// Otherwise, it stores and returns the given value.
// The loaded result is true if the value was loaded, false if stored.
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {
// Avoid locking if it's a clean hit.
read, _ := m.read.Load().(readOnly)
//如果read里面有
if e, ok := read.m[key]; ok {
//tryLoadOrStore逻辑:
//tryLoadOrStore:先load
//if e.p=expunged, ret nil, false, false
//if e.p=nil, 返回原来的值,true, true
//else store,return value, false, true
actual, loaded, ok := e.tryLoadOrStore(value)
if ok {
return actual, loaded
}
//如果read读到了,并且e.p=expunged,则往下执行,走dirty
}

m.mu.Lock()
read, _ = m.read.Load().(readOnly)
//如果从read读到
if e, ok := read.m[key]; ok {
//unexpungeLocked: return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
if e.unexpungeLocked() {
//If the entry was previously expunged, it must be added to the dirty map
//before m.mu is unlocked.
m.dirty[key] = e
}
actual, loaded, _ = e.tryLoadOrStore(value)
//如果从dirty读到
} else if e, ok := m.dirty[key]; ok {
actual, loaded, _ = e.tryLoadOrStore(value)
m.missLocked()
} else {
if !read.amended {
// We're adding the first new key to the dirty map.
// Make sure it is allocated and mark the read-only map as incomplete.
m.dirtyLocked()
m.read.Store(readOnly{m: read.m, amended: true})
}
m.dirty[key] = newEntry(value)
actual, loaded = value, false
}
m.mu.Unlock()

return actual, loaded
}

note

load和store的结合体,不赘述

Delete

1
2
3
4
// Delete deletes the value for a key.
func (m *Map) Delete(key interface{}) {
m.LoadAndDelete(key)
}

LoadAndDelete

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// LoadAndDelete deletes the value for a key, returning the previous value if any.
// The loaded result reports whether the key was present.
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
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 {
e, ok = m.dirty[key]
delete(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 {
//if e.p=nil/expunged,ret
//else set e.p = nil
return e.delete()
}
return nil, false
}

note

  • 如果read中找到了,直接在read标记删除,否则才去找dirty
  • read的删除,对于nil/expunged直接ret,不然标记为nil
  • dirty的删除,才是真的删除
  • 删除dirty也会使misscount+1

Range

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Range calls f sequentially for each key and value present in the map.
// If f returns false, range stops the iteration.
//
// Range does not necessarily correspond to any consistent snapshot of the Map's
// contents: no key will be visited more than once, but if the value for any key
// is stored or deleted concurrently, Range may reflect any mapping for that key
// from any point during the Range call.
//
// Range may be O(N) with the number of elements in the map even if f returns
// false after a constant number of calls.
func (m *Map) Range(f func(key, value interface{}) bool) {
// We need to be able to iterate over all of the keys that were already
// present at the start of the call to Range.
// If read.amended is false, then read.m satisfies that property without
// requiring us to hold m.mu for a long time.
read, _ := m.read.Load().(readOnly)
if read.amended {
// m.dirty contains keys not in read.m. Fortunately, Range is already O(N)
// (assuming the caller does not break out early), so a call to Range
// amortizes an entire copy of the map: we can promote the dirty copy
// immediately!
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
if read.amended {
//如果dirty有新数据,则先同步到read在range
read = readOnly{m: m.dirty}
m.read.Store(read)
m.dirty = nil
m.misses = 0
}
m.mu.Unlock()
}

for k, e := range read.m {
v, ok := e.load()
if !ok {
continue
}
if !f(k, v) {
break
}
}
}

dirtyLocked

1
2
3
4
5
6
7
8
9
10
11
12
13
func (m *Map) dirtyLocked() {
if m.dirty != nil {
return
}

read, _ := m.read.Load().(readOnly)
m.dirty = make(map[interface{}]*entry, len(read.m))
for k, e := range read.m {
if !e.tryExpungeLocked() {
m.dirty[k] = e
}
}
}

entry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

// An entry is a slot in the map corresponding to a particular key.
type entry struct {
// p points to the interface{} value stored for the entry.
//
// If p == nil, the entry has been deleted and m.dirty == nil.
//
// If p == expunged, the entry has been deleted, m.dirty != nil, and the entry
// is missing from m.dirty.
//
// Otherwise, the entry is valid and recorded in m.read.m[key] and, if m.dirty
// != nil, in m.dirty[key].
//
// An entry can be deleted by atomic replacement with nil: when m.dirty is
// next created, it will atomically replace nil with expunged and leave
// m.dirty[key] unset.
//
// An entry's associated value can be updated by atomic replacement, provided
// p != expunged. If p == expunged, an entry's associated value can be updated
// only after first setting m.dirty[key] = e so that lookups using the dirty
// map find the entry.
p unsafe.Pointer // *interface{}
}

tryStore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// tryStore stores a value if the entry has not been expunged.
//
// If the entry is expunged, tryStore returns false and leaves the entry
// unchanged.
func (e *entry) tryStore(i *interface{}) bool {
for {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
return true
}
}
}

unexpungeLocked

1
2
3
4
5
6
7
// unexpungeLocked ensures that the entry is not marked as expunged.
//
// If the entry was previously expunged, it must be added to the dirty map
// before m.mu is unlocked.
func (e *entry) unexpungeLocked() (wasExpunged bool) {
return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}

storeLocked

1
2
3
4
5
6
// storeLocked unconditionally stores a value to the entry.
//
// The entry must be known not to be expunged.
func (e *entry) storeLocked(i *interface{}) {
atomic.StorePointer(&e.p, unsafe.Pointer(i))
}

tryLoadOrStore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// tryLoadOrStore atomically loads or stores a value if the entry is not
// expunged.
//
// If the entry is expunged, tryLoadOrStore leaves the entry unchanged and
// returns with ok==false.
func (e *entry) tryLoadOrStore(i interface{}) (actual interface{}, loaded, ok bool) {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return nil, false, false
}
if p != nil {
return *(*interface{})(p), true, true
}

// Copy the interface after the first load to make this method more amenable
// to escape analysis: if we hit the "load" path or the entry is expunged, we
// shouldn't bother heap-allocating.
ic := i
for {
if atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&ic)) {
return i, false, true
}
p = atomic.LoadPointer(&e.p)
if p == expunged {
return nil, false, false
}
if p != nil {
return *(*interface{})(p), true, true
}
}
}

delete

1
2
3
4
5
6
7
8
9
10
11
func (e *entry) delete() (value interface{}, ok bool) {
for {
p := atomic.LoadPointer(&e.p)
if p == nil || p == expunged {
return nil, false
}
if atomic.CompareAndSwapPointer(&e.p, p, nil) {
return *(*interface{})(p), true
}
}
}

tryExpungeLocked

1
2
3
4
5
6
7
8
9
10
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
}

总结

  • misscount达到阈值,将dirty刷回read,set dirty=nil
  • 新增元素的时候(区别与修改),如果amend=false,则将read刷回dirty

reference

[1]https://colobu.com/2017/07/11/dive-into-sync-Map/

[2]https://draveness.me/golang/docs/part2-foundation/ch03-datastructure/golang-hashmap/

[3]https://juejin.im/post/6844903895227957262

----------- ending -----------