sync.Map原始碼分析

chasiny發表於2018-06-07

部落格地址: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 寫入就稍微麻煩很多了

  1. 首先會先判斷鍵值是否已經存在read中,存在的話便嘗試直接寫入(read不只是讀,此時被寫入),由於從read獲取的是entry指標,因此對從read讀取entry進行修改便相當於修改dirty中對應的entry,此時寫入的是使用原子操作。
  2. 鍵值存在在read中並且該entry被標記為expunged(這種情況出現在從read複製資料到dirty中,看tryExpungeLocked函式,將所有鍵為nil置為expunged,表示該鍵被刪除,但沒有在dirty中)
  3. 從read複製到dirty的過程來說,主要是用dirtyLocked函式實現的,複製除了entry為nil跟expunged的資料

參考

相關文章