go sync.map 源码分析
一 簡言
1.1?文件所在目錄:go/src/sync/map.go
1.2 本篇博客的go版本:go1.10.3
二 原理說明
二 ?簡單使用
三 源碼分析
基本結構體的定義
// Map就像一個map[interface{}]interface{},但是是并發安全的 // 針對兩種情況進行了優化 // 1. 只寫入一次,但多次讀取 // 2. 多協程讀取,寫入,覆蓋不同的key type Map struct {// 鎖,訪問dirty時才會加鎖mu Mutex// read 包含了map中可以安全并發訪問的部分(使用或者不使用mu鎖)// read 可以總是安全的load,但是store時必須和mu一起使用// read中的entry可以不加鎖,并發地更新,但是想要更新一個之前被置為expunged的entry,那么必須加入到dirty中,保持和read一致read atomic.Value // 下面的readOnly對象// dirty包含了map中需加鎖才能訪問的key// 被置為enxgunged的entry不會保存在dirty中// 若dirty為nil,read中的amended為false;若dirty不為nil,read中的amended為truedirty map[interface{}]*entry// misses 統計了read中讀取key不成功,需加鎖后判斷dirty中是否存在key的次數// 一旦未命中次數和dirty長度一樣時,平均每個key未命中一次,也就是說加鎖消耗達到了拷貝一次整個dirty,就把dirty提升為readmisses int }type readOnly struct {m map[interface{}]*entryamended bool // true表dirty中包含read中不存在的key }// expunged 是一個隨意的指針,用來標記dirty map中的一個entry已經被刪除,read中一個entry被刪除時,是置為nil var expunged = unsafe.Pointer(new(interface{}))type entry struct {// p指向存儲的interface值// 若p為nil,表明該entry在read中被刪除了,但dirty中還在,所以能直接更新值// 若p為expunged,表明該entry不存在于dirty中(因為只有dirty新建時,p才有可能被置為expunged,具體見tryExpungeLocked函數),所以更新時要把這個entry復制到dirty中// 其他情況,該entry有效,且記錄在read中,若dirty不為空,那么dirty也存在該entry// 一個entry可以通過使用原子操作替換為nil來刪除// 一個entry對應的值,可以通過原子操作來替換// 若p為expunged,一個entry的對應值可以被更新,只有在第一次設置m.dirty[key] = e后p unsafe.Pointer // *interface{} }// 為i(其實是key對應的value值)新建一個entry,其中p保存的是i的地址 func newEntry(i interface{}) *entry {return &entry{p: unsafe.Pointer(&i)} }3.1 新建,修改(Store(),LoadOrStore()),源碼及分析
// 存儲key,value func (m *Map) Store(key, value interface{}) {// 先從read中讀取,存在則嘗試存儲,成功則返回read, _ := m.read.Load().(readOnly)if e, ok := read.m[key]; ok && e.tryStore(&value) {return}// read中不存在key,或者嘗試存儲時失敗,加鎖m.mu.Lock()// 雙重檢測(因為加鎖過程中,dirty可能被提升為read,dirty被置為nil,此時read中可能存在該key了)read, _ = m.read.Load().(readOnly)if e, ok := read.m[key]; ok { // 若read中存在該key// 若被標記為expunged(函數內已更改為nil),說明dirty中不存在該entry(因為e.p置為expunged只有一處,即dirty時新建/重建時調用的tryExpungeLocked函數,并未添加到dirty中),所以此時需在dirty中添加該keyif e.unexpungeLocked() {m.dirty[key] = e}// 修改entry的值,注意:修改后read,dirty中都被修改了,因為read,dirty中保存的時entry的指針,這里是通過指針修改的e.storeLocked(&value)} else if e, ok := m.dirty[key]; ok { // dirty中存在了,則更新dirty中e.storeLocked(&value)} else { // dirty中也沒有該key// 若dirty中不包含read中不存在的key,說明dirty還未新建或重建,此時應創建dirtyif !read.amended {// 新建dirtym.dirtyLocked()// 新建read,并把amended標記為true,因此添加該key后,dirty中就包含了read中不包含的key了m.read.Store(readOnly{m: read.m, amended: true})}// dirty中為該key新建一個entry條目m.dirty[key] = newEntry(value)}m.mu.Unlock() }// 為該條目嘗試存儲值i(多協程操作,e.p的值可能有多種情況,所以需for循環使用CAS) // 若已刪除,直接返回false // 若未刪除,則循環使用CAS設置值;即使為nil,也可以賦值 func (e *entry) tryStore(i *interface{}) bool {p := atomic.LoadPointer(&e.p)// 若已刪除,則直接返回if p == expunged {return false}// 未刪除,則CAS操作,設置該值for {// CAS賦值if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {return true}// 設置失敗,則重新獲取值p = atomic.LoadPointer(&e.p)// 若已刪除,則說明其他協程剛剛刪除了該值,這里不可再設置if p == expunged {return false}} }// 為該entry無條件存儲值i(此時mu鎖是鎖定的,函數Locked結尾即標明;調用時e.p不能是expunged,使用原子操作賦值) func (e *entry) storeLocked(i *interface{}) {atomic.StorePointer(&e.p, unsafe.Pointer(i)) }// unexpungeLocked 判斷本entry是否為expunged狀態(此時mu鎖是鎖定的,判斷規則見下面) // 若該entry的value為expunged,說明已不在dirty中(因為e.p置為expunged只有一處,即dirty時新建時調用的tryExpungeLocked函數,那里并未復制到dirty中), // 在mu鎖解鎖之前,必須要重新加入到dirty中,這里置為nil,返回true // 若該entry的value不為expunged,則返回false func (e *entry) unexpungeLocked() (wasExpunged bool) {return atomic.CompareAndSwapPointer(&e.p, expunged, nil) }// 新建dirty(此時mu鎖是鎖定的) func (m *Map) dirtyLocked() {if m.dirty != nil {return}// 獲取readread, _ := m.read.Load().(readOnly)// 新建dirtym.dirty = make(map[interface{}]*entry, len(read.m))// 遍歷read,逐個值拷貝給dirty// 不能直接整個map賦值,是因為read中可能有些entry已經被標記為刪除(為nil),我們只需復制那些未被刪除的// 注意:若entry為nil,則置為expunged,標記為徹底刪除,不再復制到dirty中for k, e := range read.m {if !e.tryExpungeLocked() {m.dirty[k] = e}} }// 嘗試刪除本entry(此時mu鎖是鎖定的) // 返回值表本entry是否已刪除 func (e *entry) tryExpungeLocked() (isExpunged bool) {// 原子地重新讀取p := atomic.LoadPointer(&e.p)// 指針為nil時(此時mu是鎖定的,為什么還要for循環來嘗試CAS呢,不懂?????)for p == nil {// 用CAS操作,置為expunged,以此來標記本條目已被徹底刪除,成功則返回if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {return true}// CAS失敗后再獲取存儲的指針,若為nil,則一直嘗試;若不為nil了,則跳出p = atomic.LoadPointer(&e.p)}// 若為expunged說明,已被標記刪除,否則,未刪除return p == expunged }// LoadOrStore 若key存在,則返回對應的value,loaded為true // 若key不存在,則存儲給定的value值;loaded為false func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {// 先嘗試從read中讀取,若read中存在,則嘗試loadorstore,成功則返回read, _ := m.read.Load().(readOnly)if e, ok := read.m[key]; ok {actual, loaded, ok := e.tryLoadOrStore(value)if ok {return actual, loaded}}// read中不存在該key,或存在但tryLoadOrStore失敗,則加鎖m.mu.Lock()// 雙重檢測(因為加鎖過程中,dirty可能被提升為read,dirty被置為nil,此時read中可能存在該key了)read, _ = m.read.Load().(readOnly)// read中已存在該keyif e, ok := read.m[key]; ok {// 若為expunged狀態,說明dirty中已被刪除,需添加到dirty中if e.unexpungeLocked() {m.dirty[key] = e}actual, loaded, _ = e.tryLoadOrStore(value) // 更新后read,dirty中都是最新的} else if e, ok := m.dirty[key]; ok { // dirty中存在該key// 在dirty中嘗試loadorstoreactual, loaded, _ = e.tryLoadOrStore(value)// 未命中時的處理m.missLocked()} else { // dirty中也不存在該key// 若此時dirty中不包含read中不存在的key,(即dirty中的key在read中都存在),說明是第一次新加key到dirty中if !read.amended {// 建立dirtym.dirtyLocked()// amended 置為true,因為此次添加key后,dirty中就包含了read中不存在的keym.read.Store(readOnly{m: read.m, amended: true})}// dirty中新建該keym.dirty[key] = newEntry(value)actual, loaded = value, false}m.mu.Unlock()return actual, loaded }注意事項:
1. Store()時,若read中有,且不為expunged,則優先在read中修改
2. 若read中不存在,dirty中存在,則修改dirty中的;若dirty中不存在,可能需要新建/重建dirty,此時amended為true
3. Store() 可以多協程環境下調用,需多注意并發問題,比如判斷read是否有key的雙重檢測
3.2 讀取(Load(),LoadOrStore()),?源碼及分析
// 讀取key的值 func (m *Map) Load(key interface{}) (value interface{}, ok bool) {read, _ := m.read.Load().(readOnly)// 先嘗試從read中讀取e, ok := read.m[key]// read中不存在,且dirty中包含read中不存在的keyif !ok && read.amended {// 加鎖m.mu.Lock()// 雙重檢測(因為加鎖過程中,可能dirty被提升為read,dirty被置為nil,此時read中可能有該key了)read, _ = m.read.Load().(readOnly)e, ok = read.m[key]// read中仍然不存在該key,且dirty中包含read中不存在的keyif !ok && read.amended {e, ok = m.dirty[key]// 無論dirty中是否存在該key,都要記錄一次未命中,然后根據情況,決定是否把dirty提升為readm.missLocked()}m.mu.Unlock()}// 仍然不存在,返回if !ok {return nil, false}// 通過entry加載數據return e.load() }// 從entry中加載值 // 值為nil,expunged時,說明已經被刪除 func (e *entry) load() (value interface{}, ok bool) {// 原子地重新讀取值p := atomic.LoadPointer(&e.p)if p == nil || p == expunged {return nil, false}// 先轉換為接口指針,再取值return *(*interface{})(p), true }// 未命中時的處理(此時mu鎖是鎖定的) func (m *Map) missLocked() {m.misses++// 未命中次數沒有達到dirty時,返回,即平均每個key達到未命中一次if m.misses < len(m.dirty) {return}// 把dirty提升為read,直接整個map賦值,里面的amended未賦值,即默認false,表dirty中不包含read中不存在的key,因為下面dirty就要被置為nil了m.read.Store(readOnly{m: m.dirty})m.dirty = nilm.misses = 0 }// LoadOrStore 若key存在,則返回對應的value,loaded為true // 若key不存在,則存儲給定的value值;loaded為false func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {// 先嘗試從read中讀取,若read中存在,則嘗試loadorstore,成功則返回read, _ := m.read.Load().(readOnly)if e, ok := read.m[key]; ok {actual, loaded, ok := e.tryLoadOrStore(value)if ok {return actual, loaded}}// read中不存在該key,或存在但tryLoadOrStore失敗,則加鎖m.mu.Lock()// 雙重檢測(因為加鎖過程中,dirty可能被提升為read,dirty被置為nil,此時read中可能存在該key了)read, _ = m.read.Load().(readOnly)// read中已存在該keyif e, ok := read.m[key]; ok {// 若為expunged狀態,說明dirty中已被刪除,需添加到dirty中if e.unexpungeLocked() {m.dirty[key] = e}actual, loaded, _ = e.tryLoadOrStore(value) // 更新后read,dirty中都是最新的} else if e, ok := m.dirty[key]; ok { // dirty中存在該key// 在dirty中嘗試loadorstoreactual, loaded, _ = e.tryLoadOrStore(value)// 未命中時的處理m.missLocked()} else { // dirty中也不存在該key// 若此時dirty中不包含read中不存在的key,(即dirty中的key在read中都存在),說明是第一次新加key到dirty中if !read.amended {// 建立dirtym.dirtyLocked()// amended 置為true,因為此次添加key后,dirty中就包含了read中不存在的keym.read.Store(readOnly{m: read.m, amended: true})}// dirty中新建該keym.dirty[key] = newEntry(value)actual, loaded = value, false}m.mu.Unlock()return actual, loaded }// tryLoadOrStore(多協程操作,此時mu鎖未鎖定) // 若該條目未被刪除,原子地讀取value值,loaded=true,ok=true // 若該條目已被刪除(expunged或nil),則tryLoadOrStore函數不修改,直接返回,loaded==false,ok==false func (e *entry) tryLoadOrStore(i interface{}) (actual interface{}, loaded, ok bool) {p := atomic.LoadPointer(&e.p)// 已被刪除,dirty中不存在該entry,不可賦值if p == expunged {return nil, false, false}// 正常有效值,則返回其值if p != nil {return *(*interface{})(p), true, true}// 下面是為nil值的情況// 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 := ifor {// CAS操作,賦值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}} }注意事項:
1. Load()時,若read中存在,直接讀取值,返回
2. 若read中不存在,則未命中次數+1,累計次數達到dirty長度時,也就是平均每個key達到一次未命中,則把dirty提升為read
3. Load(),LoadOrStore() 可以多協程環境下調用,需多注意并發問題,比如判斷read是否有key的雙重檢測
3.3?刪除(Delete()),?源碼及分析
// 刪除一個key func (m *Map) Delete(key interface{}) {// 優先從read中讀取read, _ := m.read.Load().(readOnly)e, ok := read.m[key]// read中不存在,但dirty中包含read中不存在的key時,需要從dirty中刪除if !ok && read.amended {// 必加鎖m.mu.Lock()// 雙重檢測(因為加鎖的過程中dirty可能被提升為read,dirty被置為nil,此時read中可能有該key了)read, _ = m.read.Load().(readOnly)e, ok = read.m[key]// 仍然不存在,但dirty中包含read中不存在的key時,從dirty中刪除(在鎖定中,所以可以真正刪除)if !ok && read.amended {delete(m.dirty, key)}m.mu.Unlock()}// 存在時,一定是存在于read中,只需通過entry標記為已刪除(置為nil)if ok {e.delete()} }// 標記一個條目刪除,置為nil(mu鎖未加鎖,需考慮多協程安全,本entry此時存在于read中) func (e *entry) delete() (hadValue bool) {for {// 原子地重新讀取p := atomic.LoadPointer(&e.p)// nil 表已被其他協程通過Delete()標記刪除,不再操作// expunged 表已被其他協程提升dirty為read時,標記刪除,不再操作if p == nil || p == expunged {return false}// CAS操作,置為nilif atomic.CompareAndSwapPointer(&e.p, p, nil) {return true}} }注意事項:
1. Delete()時,若read中存在,直接在read中標記為刪除(置nil),并非真正的立即刪掉,這樣可以下次賦值時直接操作,達到重復使用的目的
2. 若read中存在key,標記刪除后,此時dirty里面的該key是不準確的,這種不準確是臨時的,因為每次往dirty中新加入key的時候,會先判斷dirty是否為nil,為空則新建,且從read中篩選一遍,這樣dirty又是準確的了;以后dirty被提升為read時,為nil的會被置為expunged,來標記為徹底刪除,不會壓入到dirty中
3. 若dirty中存在key,則徹底刪除掉
4. Delete()可以多協程環境下調用,需多注意并發問題,比如判斷read是否有key的雙重檢測,比如置為nil時的for循環CAS操作
5. 切記:dirty為nil 和 read.amended 為false,是同時存在的
? ? 要么dirty = nil,read.amended = false,要么dirty != nil,amended = true
總結
以上是生活随笔為你收集整理的go sync.map 源码分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: go 原子操作 atomic
- 下一篇: go 并发安全map 分段锁实现