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. 分摊分析。
  • 相关阅读:
    PIE SDK 基于Dot net bar实现比例尺控件
    PIE SDK 监督分类对话框类(SupervisedClassificaitonDialog)使用经验
    图层树右键菜单结合Command操作过程
    PIE 插件式开发小笔记__PIESDK学习体会
    [转]sqlserver收缩文件没效果的解决办法
    efcore 输出显示sql语句
    Linux 常见的进程调度算法
    Linux 配置 vimrc
    排序 选择排序&&堆排序
    C/C++ 内存管理 (《高质量C++》-- 整理笔记)
  • 原文地址:https://www.cnblogs.com/ExMan/p/12421756.html
Copyright © 2011-2022 走看看