zoukankan      html  css  js  c++  java
  • sync.Map实现分析

    golang的SDK中提供线程安全的map实现sync.Map。它是针对RWMutex+map的实现方案中存在cache line的false share提出来的。主要适用于两个场景:

    1. 针对一个key一次写多次读。
    2. 多个goroutine并发读写修改的key是没有交集。

    在这两种情况下,相比一个Mutex或者RWMutex加上普通的map,锁的竞争要少的多。那为什么呢?

    数据结构

    type Map struct {
      mu Mutex
    
      // read contains the portion of the map's contents that are safe for
      // concurrent access (with or without mu held).
      //
      // The read field itself is always safe to load, but must only be stored with
      // mu held.
      //
      // Entries stored in read may be updated concurrently without mu, but updating
      // a previously-expunged entry requires that the entry be copied to the dirty
      // map and unexpunged with mu held.
      read atomic.Value // readOnly
    
      // dirty contains the portion of the map's contents that require mu to be
      // held. To ensure that the dirty map can be promoted to the read map quickly,
      // it also includes all of the non-expunged entries in the read map.
      //
      // Expunged entries are not stored in the dirty map. An expunged entry in the
      // clean map must be unexpunged and added to the dirty map before a new value
      // can be stored to it.
      //
      // If the dirty map is nil, the next write to the map will initialize it by
      // making a shallow copy of the clean map, omitting stale entries.
      dirty map[interface{}]*entry
    
      // misses counts the number of loads since the read map was last updated that
      // needed to lock mu to determine whether the key was present.
      //
      // Once enough misses have occurred to cover the cost of copying the dirty
      // map, the dirty map will be promoted to the read map (in the unamended
      // state) and the next store to the map will make a new dirty copy.
      misses int
    }
    
    // readOnly is an immutable struct stored atomically in the Map.read field.
    type readOnly struct {
      m       map[interface{}]*entry
      amended bool // true if the dirty map contains some key not in m.
    }
    
    // An entry is a slot in the map corresponding to a particular key.
    type entry struct {
      // p points to the interface{} value stored for the entry.
      //
      // If p == nil, the entry has been deleted and m.dirty == nil.
      //
      // If p == expunged, the entry has been deleted, m.dirty != nil, and the entry
      // is missing from m.dirty.
      //
      // Otherwise, the entry is valid and recorded in m.read.m[key] and, if m.dirty
      // != nil, in m.dirty[key].
      //
      // An entry can be deleted by atomic replacement with nil: when m.dirty is
      // next created, it will atomically replace nil with expunged and leave
      // m.dirty[key] unset.
      //
      // An entry's associated value can be updated by atomic replacement, provided
      // p != expunged. If p == expunged, an entry's associated value can be updated
      // only after first setting m.dirty[key] = e so that lookups using the dirty
      // map find the entry.
      p unsafe.Pointer // *interface{}
    }
    

    Map.read包含了部分数据,读写请求优先考虑read,针对它的操作都是CAS,无锁的。

    Map.dirty包含的数据是read的超集,对他的操作需要加锁。

    readOnly.m表示当前read的数据,readOnly.amended表示是否有数据在dirty中。

    entry保存具体数值的指针。有三种情况:

    1. nil,表示已经删除,这个时候dirtyentry的值也是nil,因为他们是同一个entry的地址。
    2. expunged,表示数据已经擦除,entry不在dirty中。
    3. 具体的数据值,一定会在dirty中。

    接口

    sync.Map包含五个接口:LoadStoreLoadOrStoreDeleteRange

    Load、Store、LoadOrStore和Delete

    这几个接口都有类似的模式:

    read, _ := m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok {
    	ret := do_the_operation()
    	if ret is success {
    		return
    	}
    }
    m.mu.Lock()
    read, _ := m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok {
    	ret := do_the_operation()
    } else if e, ok := m.dirty[key]; ok {
    	ret := do_the_operation()
    }
    m.mu.Unlock()
    

    利用read的CAS操作减少锁并发,同时由于并发存在获取锁之后还是有可能数据已经在read中,因此还是对read再做一次同样的操作,复用内存。如果数据仍然不在read中才会考虑操作dirty。针对数据是否在read中这几个接口的逻辑如下:

    read包含了部分数据,如果key存在并而且与它对应的entry不是expunged,数据操作优先在这里进行。

    1. Store:直接更新entry值。
    2. Load:直接返回entry值。
    3. LoadOrStore:针对entrytryLoadOrStore操作。
    4. Delete:把entry设置成nil

    当数据不在read中时,就会涉及到dirty了:

    1. Store
      a. 如果entryread中并且是expunged,则复用,同时把它修改成nil,然后把entry赋值到dirty,这样就避免了只在read不在dirty的情况。
      b. 如果entrydirty中,那么更新entry值。
      c. 如果entry也不在dirty中,如果dirtynil,则复制read中的entry值非nil的数据。然后,添加值到dirty
    2. Load:从dirty查找,同时增加misses。如果超过一定的阀值,就会发生数据从dirtyread的迁移。
    3. LoadOrStore:流程和Store接口类似,只是返回值和对entry的处理逻辑不一样。
      a. 如果entry有值,则返回具体值以及存在的标识。
      b. 如果entry值为nil,设置entry为新的值并返回它和不存在标识。
      c. 如果entry值是expunged,则返回nil和存在标志,这个是比较特殊。在LoadOrStore同时,并发的把entryread复制到dirty,这种情况就会发生。

    Range

    Range接口相对简单,如果有部分数据在dirty中就会把dirty的数据提升到read中,并重置dirty。然后,遍历的是dirty数据。否则,只遍历read中的数据。这里不保证能遍历到之后添加的数据。

    通过上面的逻辑我们发现readdirty直接数据流转逻辑如下:

    1. readdirty:在StoreLoadOrStore的时候,如果需要保存的key既不在read也不在dirty,而且这时dirtynil,就会把read中的nil数据变成expunged,并复制除了这份以外的数据到dirty
    2. dirtyread
      a. 在LoadLoadOrStore的时候,如果read中不存在,需要从dirty中获取数据,就会增加misses,当misses等于dirty的大小时,就会把dirty封装成readOnly,然后原子的赋值给read,并重置dirty
      b. 在Range的时候,如果有数据不在read中同样会把dirty封装成readOnly,然后原子的赋值给read,并重置dirty数据。

    疑问

    1. 为什么需要expunged状态?
      > 如果没有这个状态,更新已经删除的但是已经存在的数据就需要加锁了。

    2. 为什么newEntry的时候取的是参数interface{}的地址,这个地址不是栈上的么,会不会有问题?
      > 参数i的地址被保存到map中时,变量&i已经逃逸到堆上面去了。

    总结

    文中开头提到的两个主要使用场景的原因主要使用的以下技术:

      1. 无锁的CAS操作。
      2. 读写分离,通过一份只读的数据结合CAS操作减少锁竞争。
      3. 延迟删除,只有当只读的数据被写的数据覆盖以后才会被gc回收。
      4. 内存复用,已经删除的数据所在的内存,当同一个key赋值的时候,可以被重新被使用。
      5. 分摊分析。
  • 相关阅读:
    Yield Usage Understanding
    Deadclock on calling async methond
    How to generate file name according to datetime in bat command
    Run Unit API Testing Which Was Distributed To Multiple Test Agents
    druid的关键参数+数据库连接池运行原理
    修改idea打开新窗口的默认配置
    spring boot -thymeleaf-url
    @pathvariable和@RequestParam的区别
    spring boot -thymeleaf-域对象操作
    spring boot -thymeleaf-遍历list和map
  • 原文地址:https://www.cnblogs.com/ExMan/p/12421756.html
Copyright © 2011-2022 走看看