你不得不知道的sync.Map源碼分析

來源:互聯網
上載者:User

sync.Map源碼分析

背景

眾所周知,go普通的map是不支援並發的,換而言之,不是線程(goroutine)安全的。博主是從golang 1.4開始使用的,那時候map的並發讀是沒有支援,但是並發寫會出現髒資料。golang 1.6之後,並發地讀寫會直接panic:

fatal error: concurrent map read and map write
package mainfunc main() {    m := make(map[int]int)    go func() {        for {            _ = m[1]        }    }()    go func() {        for {            m[2] = 2        }    }()    select {}}

所以需要支援對map的並發讀寫時候,博主使用兩種方法:

  1. 第三方類庫 concurrent-map。
  2. map加上sync.RWMutex來保障線程(goroutine)安全的。

golang 1.9之後,go 在sync包下引入了並發安全的map,也為博主提供了第三種方法。本文重點也在此,為了時效性,本文基於golang 1.10源碼進行分析。

sync.Map

結構體

Map

type Map struct {    mu Mutex    //互斥鎖,用於鎖定dirty map    read atomic.Value //優先讀map,支援原子操作,注釋中有readOnly不是說read是唯讀,而是它的結構體。read實際上有寫的操作    dirty map[interface{}]*entry // dirty是一個當前最新的map,允許讀寫    misses int // 主要記錄read讀取不到資料加鎖讀取read map以及dirty map的次數,當misses等於dirty的長度時,會將dirty複製到read}

readOnly

readOnly 主要用於儲存,通過原子操作儲存在Map.read中元素。

type readOnly struct {    m       map[interface{}]*entry    amended bool // 如果資料在dirty中但沒有在read中,該值為true,作為修改標識}

entry

type entry struct {    // nil: 表示為被刪除,調用Delete()可以將read map中的元素置為nil    // expunged: 也是表示被刪除,但是該鍵只在read而沒有在dirty中,這種情況出現在將read複製到dirty中,即複製的過程會先將nil標記為expunged,然後不將其複製到dirty    //  其他: 表示存著真正的資料    p unsafe.Pointer // *interface{}}

原理

如果你接觸過大Java,那你一定對CocurrentHashMap利用鎖分段技術增加了鎖的數目,從而使爭奪同一把鎖的線程的數目得到控制的原理記憶深刻。
那麼Golang的sync.Map是否也是使用了相同的原理呢?sync.Map的原理很簡單,使用了空間換時間策略,通過冗餘的兩個資料結構(read、dirty),實現加鎖對效能的影響。
通過引入兩個map將讀寫分離到不同的map,其中read map提供並發讀和已存元素原子寫,而dirty map則負責讀寫。 這樣read map就可以在不加鎖的情況下進行並發讀取,當read map中沒有讀取到值時,再加鎖進行後續讀取,並累加未命中數,當未命中數大於等於dirty map長度,將dirty map上升為read map。從之前的結構體的定義可以發現,雖然引入了兩個map,但是底層資料存放區的是指標,指向的是同一份值。

開始時sync.Map寫入資料

X=1Y=2Z=3

dirty map主要接受寫請求,read map沒有資料,此時read map與dirty map資料如。

讀取資料的時候從read map中讀取,此時read map並沒有資料,miss記錄從read map讀取失敗的次數,當misses>=len(dirty map)時,將dirty map直接升級為read map,這裡直接對dirty map進行地址拷貝並且dirty map被清空,misses置為0。此時read map與dirty map資料如。

現在有需求對Z元素進行修改Z=4,sync.Map會直接修改read map的元素。

新加元素K=5,新加的元素就需要操作dirty map了,如果misses達到閥值後dirty map直接升級為read map並且dirty map為空白map(read的amended==false),則dirty map需要從read map複製資料。

升級後的效果如下。

如果需要刪除Z,需要分幾種情況:
一種read map存在該元素且read的amended==false:直接將read中的元素置為nil。

另一種為元素剛剛寫入dirty map且未升級為read map:直接調用golang內建函數delete刪除dirty map的元素;

還有一種是read map和dirty map同時存在該元素:將read map中的元素置為nil,因為read map和dirty map 使用的均為元素地址,所以均被置為nil。

最佳化點

  1. 空間換時間。通過冗餘的兩個資料結構(read、dirty),實現加鎖對效能的影響。
  2. 使用唯讀資料(read),避免讀寫衝突。
  3. 動態調整,miss次數多了之後,將dirty資料提升為read。
  4. double-checking(雙重檢測)。
  5. 延遲刪除。 刪除一個索引值只是打標記,只有在提升dirty的時候才清理刪除的資料。
  6. 優先從read讀取、更新、刪除,因為對read的讀取不需要鎖。

方法源碼分析

Load

Load返回儲存在映射中的索引值,如果沒有值,則返回nil。ok結果指示是否在映射中找到值。

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {    // 第一次檢測元素是否存在    read, _ := m.read.Load().(readOnly)    e, ok := read.m[key]    if !ok && read.amended {        // 為dirty map 加鎖        m.mu.Lock()        // 第二次檢測元素是否存在,主要防止在加鎖的過程中,dirty map轉換成read map,從而導致讀取不到資料        read, _ = m.read.Load().(readOnly)        e, ok = read.m[key]        if !ok && read.amended {            // 從dirty map中擷取是為了應對read map中不存在的新元素            e, ok = m.dirty[key]            // 不論元素是否存在,均需要記錄miss數,以便dirty map升級為read map            m.missLocked()        }        // 解鎖        m.mu.Unlock()    }    // 元素不存在直接返回    if !ok {        return nil, false    }    return e.load()}

dirty map升級為read map

func (m *Map) missLocked() {    // misses自增1    m.misses++    // 判斷dirty map是否可以升級為read map    if m.misses < len(m.dirty) {        return    }    // dirty map升級為read map    m.read.Store(readOnly{m: m.dirty})    // dirty map 清空    m.dirty = nil    // misses重設為0    m.misses = 0}

元素取值

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}

read map主要用於讀取,每次Load都先從read讀取,當read中不存在且amended為true,就從dirty讀取資料 。無論dirty map中是否存在該元素,都會執行missLocked函數,該函數將misses+1,當m.misses < len(m.dirty)時,便會將dirty複製到read,此時再將dirty置為nil,misses=0。

storage

設定Key=>Value。

func (m *Map) Store(key, value interface{}) {    // 如果read存在這個鍵,並且這個entry沒有被標記刪除,嘗試直接寫入,寫入成功,則結束    // 第一次檢測    read, _ := m.read.Load().(readOnly)    if e, ok := read.m[key]; ok && e.tryStore(&value) {        return    }    // dirty map鎖    m.mu.Lock()    // 第二次檢測    read, _ = m.read.Load().(readOnly)    if e, ok := read.m[key]; ok {        // unexpungelocc確保元素沒有被標記為刪除        // 判斷元素被標識為刪除        if e.unexpungeLocked() {            // 這個元素之前被刪除了,這意味著有一個非nil的dirty,這個元素不在裡面.            m.dirty[key] = e        }        // 更新read map 元素值        e.storeLocked(&value)    } else if e, ok := m.dirty[key]; ok {        // 此時read map沒有該元素,但是dirty map有該元素,並需修改dirty map元素值為最新值        e.storeLocked(&value)    } else {        // read.amended==false,說明dirty map為空白,需要將read map 複製一份到dirty map        if !read.amended {            m.dirtyLocked()            // 設定read.amended==true,說明dirty map有資料            m.read.Store(readOnly{m: read.m, amended: true})        }        // 設定元素進入dirty map,此時dirty map擁有read map和最新設定的元素        m.dirty[key] = newEntry(value)    }    // 解鎖,有人認為鎖的範圍有點大,假設read map資料很大,那麼執行m.dirtyLocked()會耗費花時間較多,完全可以在操作dirty map時才加鎖,這樣的想法是不對的,因為m.dirtyLocked()中有寫入操作    m.mu.Unlock()}

嘗試儲存元素。

func (e *entry) tryStore(i *interface{}) bool {    // 擷取對應Key的元素,判斷是否標識為刪除    p := atomic.LoadPointer(&e.p)    if p == expunged {        return false    }    for {        // cas嘗試寫入新元素值        if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {            return true        }        // 判斷是否標識為刪除        p = atomic.LoadPointer(&e.p)        if p == expunged {            return false        }    }}

unexpungelocc確保元素沒有被標記為刪除。如果這個元素之前被刪除了,它必須在未解鎖前被添加到dirty map上。

func (e *entry) unexpungeLocked() (wasExpunged bool) {    return atomic.CompareAndSwapPointer(&e.p, expunged, nil)}

從read map複製到dirty map。

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 {        // 如果標記為nil或者expunged,則不複製到dirty map        if !e.tryExpungeLocked() {            m.dirty[k] = e        }    }}

LoadOrStore

如果對應的元素存在,則返回該元素的值,如果不存在,則將元素寫入到sync.Map。如果已載入值,則載入結果為true;如果已儲存,則為false。

func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {    // 不加鎖的情況下讀取read map    // 第一次檢測    read, _ := m.read.Load().(readOnly)    if e, ok := read.m[key]; ok {        // 如果元素存在(是否標識為刪除由tryLoadOrStore執行處理),嘗試擷取該元素已存在的值或者將元素寫入        actual, loaded, ok := e.tryLoadOrStore(value)        if ok {            return actual, loaded        }    }    m.mu.Lock()    // 第二次檢測    // 以下邏輯參看Store    read, _ = m.read.Load().(readOnly)    if e, ok := read.m[key]; ok {        if e.unexpungeLocked() {            m.dirty[key] = e        }        actual, loaded, _ = e.tryLoadOrStore(value)    } else if e, ok := m.dirty[key]; ok {        actual, loaded, _ = e.tryLoadOrStore(value)        m.missLocked()    } else {        if !read.amended {            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}

如果沒有刪除元素,tryLoadOrStore將自動載入或儲存一個值。如果刪除元素,tryLoadOrStore保持條目不變並返回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    }    // 如果p為nil(此處的nil,並是不是指元素的值為nil,而是atomic.LoadPointer(&e.p)為nil,元素的nil在unsafe.Pointer是有值的),則更新該元素值    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

刪除元素,採用延遲刪除,當read map存在元素時,將元素置為nil,只有在提升dirty的時候才清理刪除的數,延遲刪除可以避免後續擷取刪除的元素時候需要加鎖。當read map不存在元素時,直接刪除dirty map中的元素

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 {            // 不論dirty map是否存在該元素,都會執行刪除            delete(m.dirty, key)        }        m.mu.Unlock()    }    if ok {        // 如果在read中,則將其標記為刪除(nil)        e.delete()    }}

元素值置為nil

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        }    }}

Range

遍曆擷取sync.Map中所有的元素,使用的為快照方式,所以不一定是準確的。

func (m *Map) Range(f func(key, value interface{}) bool) {    // 第一檢測    read, _ := m.read.Load().(readOnly)    // read.amended=true,說明dirty map包含所有有效元素(含新加,不含被刪除的),使用dirty map    if read.amended {        // 第二檢測        m.mu.Lock()        read, _ = m.read.Load().(readOnly)        if read.amended {            // 使用dirty map並且升級為read map            read = readOnly{m: m.dirty}            m.read.Store(read)            m.dirty = nil            m.misses = 0        }        m.mu.Unlock()    }    // 一貫原則,使用read map作為讀    for k, e := range read.m {        v, ok := e.load()        // 被刪除的不計入        if !ok {            continue        }        // 函數返回false,終止        if !f(k, v) {            break        }    }}

總結

經過了上面的分析可以得到,sync.Map並不適合約時存在大量讀寫的情境,大量的寫會導致read map讀取不到資料從而加鎖進行進一步讀取,同時dirty map不斷升級為read map。 從而導致整體效能較低,特別是針對cache情境.針對append-only以及大量讀,少量寫情境使用sync.Map則相對比較合適。

sync.Map沒有提供擷取元素個數的Len()方法,不過可以通過Range()實現。

func Len(sm sync.Map) int {    lengh := 0    f := func(key, value interface{}) bool {        lengh++        return true    }    one:=lengh    lengh=0    sm.Range(f)    if one != lengh {        one = lengh        lengh=0        sm.Range(f)        if one <lengh {            return lengh        }            }    return one}

參考

  • Go sync.Map
  • Go 1.9 sync.Map揭秘
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.