sync.Map原始碼分析
部落格地址:sync.Map原始碼分析
普通的map
go普通的map是不支援併發的,例如簡單的寫
func main() {
wg := sync.WaitGroup{}
wg.Add(10)
m := make(map[int]int)
for i := 0; i < 10; i++ {
go func(i int) {
m[i] = i
wg.Done()
}(i)
}
wg.Wait()
}
fatal error: concurrent map writes
sync.Map
go的sync.Map幾個優化點
- 通過使用優先讀的結構體read減少鎖的衝突
- 使用雙重檢測
- 使用延遲刪除(刪除存在於read中的資料只是將其置為nil)
- 動態調整,miss次數多了之後,將dirty資料提升為read
從sync/map.go看Map的結構體
type Map struct {
mu Mutex //互斥鎖,用於鎖定dirty map
read atomic.Value //讀map,實際上不是隻讀,是優先讀
dirty map[interface{}]*entry //dirty是一個當前最新的map,允許讀寫
misses int //標記在read中沒有命中的次數,當misses等於dirty的長度時,會將dirty複製到read
}
//read儲存的實際結構體
type readOnly struct {
m map[interface{}]*entry //map
amended bool //如果有些資料在dirty中但沒有在read中,該值為true
}
type entry struct {
p unsafe.Pointer //資料指標
}
entry的幾種型別
- nil: 表示為被刪除,此時read跟dirty同時有該鍵(一般該鍵值如果存在於read中,則刪除是將其標記為nil)
- expunged: 也是表示被刪除,但是該鍵只在read而沒有在dirty中,這種情況出現在將read複製到dirty中,即複製的過程會先將nil標記為expunged,然後不將其複製到dirty
- 其他: 表示存著真正的資料
sync.Map幾種方法:
首先先說明read跟dirty不是直接存物件,而是存指標,這樣的話如果鍵值同時存在在read跟dirty中,直接原子修改read也相當於修改dirty中的值,並且當read跟dirty存在大量相同的資料時,也不會使用太多的記憶體
Load
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
if !ok && read.amended {
//如果不在read中,並且dirty有新資料,則從dirty拿
m.mu.Lock()
//雙重檢查,因為有可能在加鎖前read剛好插入該值
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
//沒有在read中,則從dirty拿
e, ok = m.dirty[key]
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
}
//當沒有命中的次數等於dirty的大小,將dirty複製給read
m.read.Store(readOnly{m: m.dirty})
m.dirty = nil
m.misses = 0
}
read主要用於讀取,每次Load都先從read讀取,當read中不存在且amended為true,就從dirty讀取資料
無論dirty是否存在該key,都會執行missLocked函式,該函式將misses+1,當misses等於dirty的大小時,便會將dirty複製到read,此時再將dirty置為nil
Delete
func (m *Map) Delete(key interface{}) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
if !ok && read.amended {
//如果不在read中,並且dirty有新資料,則從dirty中找
m.mu.Lock()
//雙重檢查
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
//這是表示鍵值只存在於dirty,直接刪除dirty中的鍵值即可
delete(m.dirty, key)
}
m.mu.Unlock()
}
if ok {
//如果在read中,則將其標記為刪除(nil)
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
}
}
}
先判斷是否在read中,不在的話再從dirty刪除
Store
func (m *Map) Store(key, value interface{}) {
//如果read存在這個鍵,並且這個entry沒有被標記刪除,嘗試直接寫入
//dirty也指向這個entry,所以修改e也可以使dirty也保持最新的entry
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
//該鍵值存在在read中
if e.unexpungeLocked() {
//該鍵值在read中被標記為抹除,則將其新增到dirty
m.dirty[key] = e
}
//更新entry
e.storeLocked(&value)
} else if e, ok := m.dirty[key]; ok {
//如果不在read中,在dirty中,則更新
e.storeLocked(&value)
} else {
//既不在read中,也不在dirty中
if !read.amended {
//從read複製沒有標記刪除的資料到dirty中
m.dirtyLocked()
m.read.Store(readOnly{m: read.m, amended: true})
}
//新增到dirty中
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
}
func (e *entry) tryStore(i *interface{}) bool {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
for {
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
return true
}
p = atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
}
}
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))
}
func (m *Map) dirtyLocked() {
if m.dirty != nil {
return
}
//從read複製到dirty
read, _ := m.read.Load().(readOnly)
m.dirty = make(map[interface{}]*entry, len(read.m))
for k, e := range read.m {
//如果標記為nil或者expunged,則不復制到dirty
if !e.tryExpungeLocked() {
m.dirty[k] = e
}
}
}
func (e *entry) tryExpungeLocked() (isExpunged bool) {
p := atomic.LoadPointer(&e.p)
for p == nil {
//嘗試將nil置為expunged
if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
return true
}
p = atomic.LoadPointer(&e.p)
}
return p == expunged
}
sync.Map 寫入就稍微麻煩很多了
- 首先會先判斷鍵值是否已經存在read中,存在的話便嘗試直接寫入(read不只是讀,此時被寫入),由於從read獲取的是entry指標,因此對從read讀取entry進行修改便相當於修改dirty中對應的entry,此時寫入的是使用原子操作。
- 鍵值存在在read中並且該entry被標記為expunged(這種情況出現在從read複製資料到dirty中,看tryExpungeLocked函式,將所有鍵為nil置為expunged,表示該鍵被刪除,但沒有在dirty中)
- 從read複製到dirty的過程來說,主要是用dirtyLocked函式實現的,複製除了entry為nil跟expunged的資料
參考
相關文章
- go sync.Map原始碼分析Go原始碼
- Go語言——sync.Map原始碼分析Go原始碼
- 不得不知道的Golang之sync.Map原始碼分析Golang原始碼
- Retrofit原始碼分析三 原始碼分析原始碼
- 集合原始碼分析[2]-AbstractList 原始碼分析原始碼
- 集合原始碼分析[1]-Collection 原始碼分析原始碼
- 集合原始碼分析[3]-ArrayList 原始碼分析原始碼
- Guava 原始碼分析之 EventBus 原始碼分析Guava原始碼
- Android 原始碼分析之 AsyncTask 原始碼分析Android原始碼
- 【JDK原始碼分析系列】ArrayBlockingQueue原始碼分析JDK原始碼BloC
- 以太坊原始碼分析(36)ethdb原始碼分析原始碼
- 以太坊原始碼分析(38)event原始碼分析原始碼
- 以太坊原始碼分析(41)hashimoto原始碼分析原始碼
- 以太坊原始碼分析(43)node原始碼分析原始碼
- 以太坊原始碼分析(52)trie原始碼分析原始碼
- 深度 Mybatis 3 原始碼分析(一)SqlSessionFactoryBuilder原始碼分析MyBatis原始碼SQLSessionUI
- 以太坊原始碼分析(51)rpc原始碼分析原始碼RPC
- 【Android原始碼】Fragment 原始碼分析Android原始碼Fragment
- 【Android原始碼】Intent 原始碼分析Android原始碼Intent
- k8s client-go原始碼分析 informer原始碼分析(6)-Indexer原始碼分析K8SclientGo原始碼ORMIndex
- k8s client-go原始碼分析 informer原始碼分析(4)-DeltaFIFO原始碼分析K8SclientGo原始碼ORM
- 以太坊原始碼分析(20)core-bloombits原始碼分析原始碼OOM
- 以太坊原始碼分析(24)core-state原始碼分析原始碼
- 以太坊原始碼分析(29)core-vm原始碼分析原始碼
- 【MyBatis原始碼分析】select原始碼分析及小結MyBatis原始碼
- redis原始碼分析(二)、redis原始碼分析之sds字串Redis原始碼字串
- ArrayList 原始碼分析原始碼
- kubeproxy原始碼分析原始碼
- [原始碼分析]ArrayList原始碼
- redux原始碼分析Redux原始碼
- preact原始碼分析React原始碼
- Snackbar原始碼分析原始碼
- React原始碼分析React原始碼
- CAS原始碼分析原始碼
- Redux 原始碼分析Redux原始碼
- SDWebImage 原始碼分析Web原始碼
- Aspects原始碼分析原始碼
- httprouter 原始碼分析HTTP原始碼