<bdo id="4g88a"><xmp id="4g88a">
  • <legend id="4g88a"><code id="4g88a"></code></legend>

    Loading

    golang sync.Map之如何設計一個并發安全的讀寫分離結構?

    在 golang中,想要并發安全的操作map,可以使用sync.Map結構,sync.Map 是一個適合讀多寫少的數據結構,今天我們來看看它的設計思想,來看看為什么說它適合讀多寫少的場景。

    如下,是golang 中sync.Map的數據結構,其中 屬性read 是 只讀的 map,dirty 是負責寫入的map,sync.Map中的鍵值對value值本質上都是entry指針類型,entry中的p才指向了實際存儲的value值。

    // sync.Map的核心數據結構
    type Map struct {
        mu Mutex                        // 對 dirty 加鎖保護,線程安全
        read atomic.Value                 // read 只讀的 map,充當緩存層
        dirty map[interface{}]*entry     // 負責寫操作的 map,當misses = len(dirty)時,將其賦值給read
        misses int                        // 未命中 read 時的累加計數,每次+1
    }
    // 上面read字段的數據結構
    type readOnly struct {
        m  map[interface{}]*entry // 
        amended bool // Map.dirty的數據和這里read中 m 的數據不一樣時,為true
    }
    
    // 上面m字段中的entry類型
    type entry struct {
        // value是個指針類型
        p unsafe.Pointer // *interface{}
    }
    

    我們從一個sync.Map的數據寫入和數據查詢 兩個過程來分析這兩個map中數據的變化。

    我將不展示具體的代碼,僅僅講述數據的流動,相信懂了這個以后再去看代碼應該不難。

    步驟一: 首先是一個初始的sync.Map 結構,我們往其中寫入數據,數據會寫到dirty中,同時,由于sync.Map 剛剛創建,所以read map還不存在,所以這里會先初始化一個read map 。amended 是read map中的一個屬性,為true代表 dirty 和read中數據不一致。

    image.png

    步驟二: 接著,如果后續再繼續寫入新數據,
    在read map沒有從dirty 同步數據之前,即amended 變為false之前,再寫入新鍵值對都只會往dirty里寫。

    image.png

    步驟三: 如果有讀操作,sync.Map 都會盡可能的讓其先讀read map,read map讀取不到并且amended 為true,即read 和dirty 數據不一致時,會去讀dirty,讀dirty的過程是上鎖的。

    image.png

    步驟四: 當讀取read map中miss次數大于等于dirty數組的長度時,會觸發dirty map整體更新為readOnly map,并且這個過程是阻塞的。更新完成后,原先dirty會被置為空,amended 為false,代表read map同步了之前所有的數據。如下圖所示,

    image.png

    整體更新的邏輯是直接替換變量的值,并非挨個復制,

    func (m *Map) missLocked() {
        m.misses++
        if m.misses < len(m.dirty) {
            return
        }
        
        // 將dirty置給read,因為穿透概率太大了(原子操作,耗時很小)
        m.read.Store(readOnly{m: m.dirty})
        m.dirty = nil
        m.misses = 0
    }
    

    步驟五: 如果后續sync.Map 不再插入新數據,那么讀取時就可以一直讀取read map中的數據了,直接讀取read map 中的key是十分高效的,只需要用atomic.Load 操作 取到readOnly map結構體,然后從中取出特定的key就行。

    如果讀miss了,因為沒有插入新數據,read.amended=false 代表read 是保存了所有的k,v鍵值對,讀miss后,也不會再去讀取dirty了,也就不會有讀dirty加鎖的過程。

    // 上面read字段的數據結構
    type readOnly struct {
        m  map[interface{}]*entry // 
        amended bool // Map.dirty的數據和這里read中 m 的數據不一樣時,為true
    }
    
    func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
        // 因read只讀,線程安全,優先讀取
        read, _ := m.read.Load().(readOnly)
        e, ok := read.m[key]
        
        // 如果read沒有,并且dirty有新數據,那么去dirty中查找(read.amended=true:dirty和read數據不一致)
        // 暫時省略 后續代碼
        .......
    	
        }
    

    上面的獲取key對應的value過程甚至比RWMutex 讀鎖下獲取map中的value還要高效,畢竟RWmutex 讀取時還需要加上讀鎖,其底層是用atomic.AddInt32 操作,而sync.Map 則是用 atomic.load 獲取map,atomic.AddInt32 的開銷比atomic.load 的開銷要大。

    ??????,所以,為什么我們說golang的sync.Map 在大量讀的情況下性能極佳,因為在整個讀取過程中沒有鎖開銷,atomic.load 原子操作消耗極低。

    但是如果后續又寫入了新的鍵值對數據,那么 dirty map中就會又插入到新的鍵值對,dirty和read的數據又不一致了,read 的amended 將改為true。

    并且由于之前dirty整體更新為read后,dirty字段置為nil了,所以,在更改amended時,也會將read中的所有未被刪除的key同步到 dirty中。

    image.png

    ??????注意,為什么在dirty整體更新一次read map后,再寫入新的鍵值對時,需要將read map中的數據全部同步到dirty,因為隨著dirty的慢慢寫入,后續讀操作又會造成讀miss的增加,最終會再次觸發dirty map整體更新為readOnly map,amended 改為false,代表read map中又有所有鍵值對數據了,也就是會回到步驟三的操作,重復步驟三到步驟五的過程。

    image.png

    只有將read map中的數據全部同步到dirty ,才能保證后續的整體更新,不會造成丟失數據。

    看到這里應該能夠明白sync.Map的適合場景了,我來總結下,

    sync.Map 適合讀多寫少的場景,大量的讀操作可以通過只讀取read map 擁有極好的性能。

    而如果寫操作增加,首先會造成read map中讀取miss增加,會回源到dirty中讀取,且dirty可能會頻繁整體更新為read,回源讀取,整體更新的步驟都是阻塞上鎖的。

    其次,寫操作也會帶來dirty和 read中數據頻繁的不一致,導致read中的數據需要同步到dirty中,這個過程在鍵值對比較多時,性能損耗較大且整個過程是阻塞的。

    所以sync.Map 并不適合大量寫操作。

    posted @ 2024-03-19 16:12  藍胖子的編程夢  閱讀(170)  評論(1編輯  收藏  舉報
    免费视频精品一区二区_日韩一区二区三区精品_aaa在线观看免费完整版_世界一级真人片
    <bdo id="4g88a"><xmp id="4g88a">
  • <legend id="4g88a"><code id="4g88a"></code></legend>