concurrent.Map: ampliar sync.Map

 

Prefacio

Recientemente escribí un proyecto de código abierto gmap , que reutiliza la tecnología sync.Map. En lugar de usar sync.Map directamente, copio el código de sync.Map y hago algunos ajustes adaptativos y extensiones. Creo que mucha gente debería preguntarse por qué copiar en lugar de usar directamente. ¿Por qué no utilizar el mapa? Aquí enumero las siguientes preguntas:

  1. El mapa agrega un bloqueo de lectura-escritura (sync.RWMutex). El problema es que el bloqueo de escritura se bloqueará cuando se use el bloqueo de lectura para atravesar el mapa. Para decirlo sin rodeos, no se puede escribir cuando se lee. La aplicación general no importa, pero para gmap o sync.Map traversal, la función de usuario debe ser llamada de nuevo y el tiempo es incontrolable. Al mismo tiempo, la función de devolución de llamada también puede acceder al mapa para formar un círculo, lo que hace que un punto muerto.
  2. No se puede agregar un bloqueo de lectura y escritura al mapa. Pensé en sync.Map, pero sync.Map no es compatible con CAS (Compare And Swap), solo LoadOrStore. Gmap proporciona a los usuarios la función CAS, que puede actualizar el valor en el mapa mediante una competencia de varias corrutinas, mientras que sync.Map solo puede almacenar la competencia de varias corrutinas.

De hecho, el diseño de sync.Map puede implementar completamente la función CAS. No sé por qué el diseñador no desarrolló la interfaz correspondiente. Creo que no es necesario, ¿o hay algún detalle que no esperaba?

El autor cree que sync.Map admite la función CAS sigue siendo un requisito común, por lo que copié el código de sync.Map, extendí una interfaz de actualización y lo cargué en https://github.com/jindezgm/concurrent/map.go . Antes de presentar la interfaz extendida, daré una introducción detallada a sync.Map para ver cómo puede atravesar el mapa y escribir operaciones al mismo tiempo. Incluso en la función de devolución de llamada transversal, se puede acceder al mapa sin interbloqueo.

Mapa de sincronización detallado

Por qué sync.Map puede implementar lectura y escritura concurrentes (aquí se refiere principalmente a la posibilidad de escribir o actualizar en el proceso de atravesar el mapa), debería ser fácil pensar en dos mapas independientes como este: copiar primero al atravesar Una copia (copia mientras atraviesa). Aunque este método es factible, presenta los siguientes problemas:

  1. Si el mapa es relativamente grande, el costo de la copia es relativamente alto;
  2. Si se recorren n corrutinas al mismo tiempo, es necesario copiarlo n veces;
  3. Si el valor se actualiza durante el proceso transversal, la corrutina transversal no puede obtener el último valor;

¿Cómo resuelve sync.Map los problemas anteriores? El autor nunca ha oído hablar de copy-on-write o copy-on-write, por el contrario, el copy-on-write es más común, incluido el kernel de Linux. Si bien sync.Map utiliza tecnología de copia en escritura, primero veamos la definición de sync.Map (el código fuente de este artículo se deriva de go1.13 / src / sync / map.go):

type Map struct {
    // 并不是读写锁,因为sync.Map采用写时复制(copy-on-write),互斥锁足够了
    // 一个只读map,一个读写map,只有访问读写map的时候需要加锁,所以不区分读写锁
    // 后文会有更详细说明,此处只是说明为什么不是读写锁
    mu Mutex
    // 源码注释是只读,这个变量存储的是只用来读map,采用原子的方式获取和设置map对象。
    // 需要重点说明的是,只读是针对map的,不是针对value的,也就是read存储的map不会
    // 有写和删除操作,只能读取,这样对这个map的并发访问就不用在考虑互斥了,因为全是读操作
    read atomic.Value 
    // dirty顾名思义就是脏了,是相对于read而言,如果read中的map是一个纯净的数据,
    // 那么dirty中就是在read的基础上或多或少的加了一点其他数据,这就是脏的来由。
    // 因为read是只读的,当新写入数据时将read拷贝至dirty中并将新数据写入到dirty,
    // 这就是写时复制,而新数据就是脏的那部分。那么问题来了,每次写新数据都要拷贝一次read么?
    // ‘脏’的数据什么时候能够变成‘干净’的?后文会给出详细解释。至于为什么是*entry而不是
    // interface{},因为entry是一个巧妙的设计,且听下文分解。
    dirty map[interface{}]*entry
    // 前面刚刚提到了脏的数据什么时候能够变干净,misses就是用来触发‘漂白’的。因为dirty比
    // read多一些脏数据,此时调用sync.Map.Load()访问这些脏数据的时候在read中找不到,我们
    // 称之为miss,就像CPU的cache miss。当miss次数达到阈值时,sync.Map就会把整个dirty
    // 赋值给read,达到漂白的效果就。这也解释了read为什么是atomic.Value类型,因为赋值的同事
    // 可能有其他协程在读取,并且也可以推导出read的map类型也是map[interface{}]*entry,
    // 否则光类型转换就非常麻烦的。
    misses int
}

Si aún no se ha dado cuenta del ingenioso diseño de sync.Map a partir de la definición, primero echemos un vistazo a una de las ingeniosas entradas de diseño de sync.Map. La interpretación de la entrada en el comentario del código fuente es una ranura de valor, que también puede entenderse como una entrada es un valor. Eche un vistazo a la definición de entrada:

type entry struct {
    // 是的,你没看错,就是这么简单,用一个指针指向对象的地址,注意是*interface{}。
    // 从dirty的类型map[interface{}]*entry可知sync.Map存储的是key:*entry对,
    // entry.p指向了value。
    p unsafe.Pointer // *interface{}
}

¿Por qué está diseñado así? La razón es muy falsa, es decir, para evitar la modificación (escribir / borrar) del mapa. Esto me recuerda un dicho famoso: "No hay diseño arquitectónico que no pueda ser resuelto por una capa de abstracción. luego otra capa de abstracción ". Una capa adicional de entrada hace que la operación de valor de sync.Map sea más flexible. Se puede eliminar directamente del mapa o de la entrada. Esto hace que la exclusión mutua se convierta en un nivel clave en lugar de un nivel de mapa (solo para exclusión mutua a nivel de lectura, sucio o de mapa), y el rendimiento general de las operaciones atómicas es muy impresionante. Por ejemplo, para generar el valor de la clave especificada, ingrese directamente entry.p = nil; después de eso, escriba el nuevo valor de la clave y luego cambie nil al puntero del nuevo valor. Los detalles específicos estarán involucrados al analizar la interfaz de sync.Map a continuación.

A continuación, presentaré otro ingenioso diseño de sync.Map: separación de lectura y escritura, primero veamos una definición de tipo:

// readOnly就是sync.Map.Read存储的类型,一个加了amended标记的map,而这个map的定义和
// sync.Map.dirty是一样的。所以可以sync.Map其实是两个map[interface{}]*entry的map,
// 一个是只读的sync.Map.read.m(后文简称read),一个是读写的sync.Map.dirty。
type readOnly struct {
    m       map[interface{}]*entry
    // true表示sync.Map.dirty中有read没有的数据,为什么需要这个标记?这样就可以避免无效加锁,
    // 因为false表示read与sync.Map.dirty相同,如果指定的key在read中不存在,
    // 也就不用从sync.Map.dirty中再找一遍了
    amended bool 
}

Debido a que read existe, se puede entender bien que sync.Map no bloqueará otras operaciones al iterar, porque está atravesando la lectura, y otras operaciones acceden a un acceso sucio o mutuamente exclusivo al valor en lectura con iteración. Por lo tanto, no habrá bloqueo. Es hora de analizar la implementación de la interfaz de sync.Map y ver si es lo mismo que nuestro entendimiento.

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
    // 现在read中查找,这个比较好理解,不需要多解释了
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]
    // 这里就可以看到amended的价值了,如果read不存在指定的key,只有dirty比read数据多,
    // 才需要查找dirty,否则没必要。
    if !ok && read.amended {
        m.mu.Lock()
        // 再次访问一次read,写过并发的同学都应该明白为什么,笔者将这称之为锁前判断锁后校验。
        // 毕竟是并发访问,当某一个协程获得锁后刚刚的判断条件可能已经被前一个获得锁的协程修改了。
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        if !ok && read.amended {
            // 在dirty中查找指定的key
            e, ok = m.dirty[key]
            // missLocked就是累加read查找miss计数,如果miss计数太多就将dirty更新到read中。
            // 就是因为有可能更新read,所以才有加锁后二次查找read。missLocked见下文。
            m.missLocked()
        }
        m.mu.Unlock()
    }
    // 把entry中的value返回
    if !ok {
        return nil, false
    }
    return e.load()
}

func (m *Map) missLocked() {
    // 看来miss的次数的阈值是dirty的中记录的数量,miss计数不仅可以在必要的时候将dirty
    // 更新到read,同时也可以避免频繁插入新值时的拷贝(write-on-copy),只要dirty
    // 还没有被更新read就不需要拷贝,因为dirty才是全集,直接插入新值即可。而miss计数的阈值
    // 设置为常数也不不合理的,因为map中的数据量大小不同阈值也是不同的,所以采用dirty中的
    // 记录的数量是一个与map数据量线性变化的动态阈值,此设计也是非常巧妙的。
    m.misses++
    if m.misses < len(m.dirty) {
        return
    }
    // 因为read是dirty的子集,所以直接将dirty赋值给read即可,为什么dirty和readOnly.m
    // 的类型相同,这样就不用转换了,方便!
    m.read.Store(readOnly{m: m.dirty})
    m.dirty = nil
    m.misses = 0
}

func (e *entry) load() (value interface{}, ok bool) {
    p := atomic.LoadPointer(&e.p)
    // 其他的代码无需解释,这里面关键需要说明的是expunged,这是一个标记变量,表示这个entry被删除,
    // 意思就是这个entry是不可用的。这么设计的目的是为了避免执行map的delete操作,因为read
    // 是不能够执行删除操作的。
    if p == nil || p == expunged {
        return nil, false
    }
    return *(*interface{})(p), true
}

Lo anterior es el código de sync.Map.Load (), que básicamente no se agotó de nuestras suposiciones anteriores, a excepción de borrado. Debido a la naturaleza inalterable de la lectura, en algunos casos resulta muy difícil eliminar un registro en lectura. Expunged se utiliza para marcar que la clave ha sido eliminada. Expunged es diferente de nil. Aunque ambos representan que el valor no existe (como se muestra en el código entry.load ()), expunged representa que la clave ya no existe y nil solo representa que el valor no existe. En cuanto a las circunstancias bajo las cuales se debe establecer la eliminación, la respuesta está en copia al escribir, echemos un vistazo al código de sync.Map.Store ():

func (m *Map) Store(key, value interface{}) {
    // 如果read中有指定的key,则直接写入到read中,entry.tryStore见下文详解
    read, _ := m.read.Load().(readOnly)
    // 此时建议读者现去阅读下面关于entry.tryStore()的解析,这样更有利于理解。
    if e, ok := read.m[key]; ok && e.tryStore(&value) {
        return
    }
    // 因为read里的没有指定的key,expunged也算作key不存在,所以只能写入dirty了
    m.mu.Lock()
    read, _ = m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok {
        // entry.unexpungeLocked()是将expunged的value设置为nil,为什么又把expunged
        // 变为nil?不是说expunged的key相当于被删除了么?这一点笔者需要说明一下,
        // 当read中的数据已经被复制到dirty中,那么read中所有entry.p=nil的记录都会被改为
        // entry.p=expunged以表示从read中删除。但需要注意的是,此时dirty中也没有这个key,
        // 此时再写入的记录的key被标记为expunged,肯定是要写入dirty的,正如下面代码所示。
        // 与此同时,因为read中已经有这个key了,重新激活它岂不是更好?这样再次访问该key的记录
        // 就直接访问read即可,这就是为什么又将expunged的记录改为nil的原因。unexpungeLocked()
        // 很简单:atomic.CompareAndSwapPointer(&e.p, expunged, nil),通过原子的方式
        // 把expunged变为nil。如果成功了,说明写时复制已经完成,可以直接写入dirty,因为只有
        // 写时复制的过程才能设置expunged;如果失败了,直接赋值entry即可。
        if e.unexpungeLocked() {
            m.dirty[key] = e
        }
        // entry.storeLocked就是调用atomic.StorePointer(),让entry.p指向value。
        // 为什么此处不用atomic.CompareAndSwapPointer()?答案是当前没有一个协程会可能
        // 同时将entry.p设置为expunged就没必要用CAS,具体为什么读者应该能够想明白。因为
        // 将entry.p设置为expunged是在写时复制过程中执行的,而这个过程必须加锁,而此处已经
        // 完成所操作,所以就直接赋值。
        e.storeLocked(&value)
        // 以上代码笔者任务可以做一点点优化,如下所示:
        //  if atomic.CompareAndSwapPointer(&e.p, expunged, unsafe.Pointer(&value)) {
        //      m.dirty[key] = e
        //  } else {
        //      e.storeLocked(&value)
        //  }
        // 以上代码省去了将expunged变为nil后再赋值的过程,而是直接将expunged改为具体的值,
        // 然后写入dirty,如果不是expunged就直接修改值。这种小优化在值为expunged情况下
        // 少一次原子写操作。
    } else if e, ok := m.dirty[key]; ok {
        // 如果dirty中有则更新dirty中的值
        e.storeLocked(&value)
    } else {
        // dirty中没有只能在dirty中插入新的记录,此处需要注意,dirty中没有有两种可能:
        // 1.dirty=nil,此时要做的就是前面说了很多遍的写时复制
        // 2.dirty!=nil,此时直接写入dirty即可
        // golang还是不错的,空指针的map依然可以查找,如果是C++早就不知道崩溃多少回了~
        // 此处用read.amended做判断笔者认为语义上并不通畅,感觉if nil == m.dirty会更好。
        // amended变量的注释是dirty是否包含一些read中没有的记录,因为只有写操作并且read中
        // 的情况下才会复制,所以只要复制amended必然为true,dirty必然非nil。此处用
        // amended逻辑上不会错误,但是笔者认为用dirty指针判断语义更顺畅。
        if !read.amended {
            // dirtyLocked就是实现写时复制的函数,见下文详解
            m.dirtyLocked()
            // 复制与amended=true是强关联,读者在解析LoadOrStore()接口的时候还会看到
            m.read.Store(readOnly{m: read.m, amended: true})
        }
        // dirty中插入新的记录,也是amended=true的原因
        m.dirty[key] = newEntry(value)
    }
    m.mu.Unlock()
}

func (e *entry) tryStore(i *interface{}) bool {
    for {
        p := atomic.LoadPointer(&e.p)
        // 这句足以证明笔者前面对于expunged的解释,他等同于key不存在
        if p == expunged {
            return false
        }
        // 通过CAS的方式多协程竞争写,为什么不是atomic.StorePointer()?因为其他协程写时复制
        // 时会把p改为expunged,一旦p为expunged就不可以在写入了。
        if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
            return true
        }
    }
}

// dirtyLocked实现写时复制
func (m *Map) dirtyLocked() {
    // 这有何必呢?在之前的用dirty指针判断多好?难道说笔者有什么细节没有注意到么?
    // 左思右想还是没有想到,哪位读者知道烦请告知笔者。
    if m.dirty != nil {
        return
    }
    // 下面的代码比较简单就是在复制read的记录,其中entry.tryExpungeLocked()是一个关键函数
    // 他是把的entry.p==nil的记录设置为entry.p=expunged,目的很简单,因为nil不会被拷贝到dirty
    // 那么read此后也不能在对其更新,否则read就不是dirty的子集了。不用再解释了,应该够明白了。
    read, _ := m.read.Load().(readOnly)
    m.dirty = make(map[interface{}]*entry, len(read.m))
    for k, e := range read.m {
        if !e.tryExpungeLocked() {
            // 没有被删除的记录被拷贝到dirty中
            m.dirty[k] = e
        }
    }
}

// tryExpungeLocked把entry.p=nil的entry标记为删除
func (e *entry) tryExpungeLocked() (isExpunged bool) {
    p := atomic.LoadPointer(&e.p)
    for p == nil {
        if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
            return true
        }
        p = atomic.LoadPointer(&e.p)
    }
    return p == expunged
}

Comprender la implementación de las interfaces Load y Store de sync.Map. Creo que la implementación de otras interfaces puede ser básicamente imaginada por los lectores, e incluso puede ser implementada por usted mismo. En vista de la integridad del artículo, el autor explica brevemente las interfaces Delete y Range, LoadOrStore Déjelo al lector.

func (m *Map) Delete(key interface{}) {
    // 首先在read里查找key是否存在
    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 {
            // read里没有,就直接删除dirty即可,因为dirty是读写的,所以直接删除
            delete(m.dirty, key)
        }
        m.mu.Unlock()
    }
    if ok {
        // 如果在read找到了,调用entry.delete(),见下文
        e.delete()
    }
}
func (e *entry) delete() (hadValue bool) {
    for {
        p := atomic.LoadPointer(&e.p)
        // 如果是nil或者expunged说明已经被删除
        if p == nil || p == expunged {
            return false
        }
        // 将value指针设置为nil
        if atomic.CompareAndSwapPointer(&e.p, p, nil) {
            return true
        }
    }
}
func (m *Map) Range(f func(key, value interface{}) bool) {
    // 如果dirty中的数据不比read多,那么直接遍历read即可
    read, _ := m.read.Load().(readOnly)
    if read.amended {
        m.mu.Lock()
        read, _ = m.read.Load().(readOnly)
        if read.amended {
            // dirty数据更多,则将整个dirty更新到read然后清空dirty
            read = readOnly{m: m.dirty}
            m.read.Store(read)
            m.dirty = nil
            m.misses = 0
        }
        m.mu.Unlock()
    }
    // 下面的代码比较简单,就不用解释了
    for k, e := range read.m {
        v, ok := e.load()
        if !ok {
            continue
        }
        if !f(k, v) {
            break
        }
    }
}

Hasta ahora, el autor básicamente resume sync.Map de la siguiente manera:

  1. Hay dos mapas, uno de solo lectura, otro de lectura y escritura sucio, el acceso a lectura es una operación sin bloqueo, el acceso a sucio requiere una operación de bloqueo;
  2. Al consultar (Cargar), primero busque leer. Si la lectura no busca sucio, si se encuentran demasiados tiempos perdidos para leer, actualice el sucio para leer y borre el sucio;
  3. Al eliminar (Eliminar), si hay una clave especificada en lectura, establezca el valor en nil; de lo contrario, elimine directamente la clave especificada en sucio
  4. Al insertar (Store), si la clave especificada en la lectura existe y el valor no se borra, entonces el nuevo valor se actualiza directamente a la lectura; si se borra el valor, se modifica a cero y se reutiliza nuevamente; si la clave no se especifica en la lectura, se ejecuta la escritura Copie e inserte el nuevo valor en sucio, y al mismo tiempo la marca modificada de lectura es verdadera;
  5. Hay dos condiciones que pueden desencadenar una actualización sucia para leer, una es el número de errores y la otra es llamar a Range. Si usa sync.Map intervalo frecuente e inserta nuevos registros, sync.Map estará ocupado con copiar- en escritura y el rendimiento disminuirá. ;

extensión sync.Map

Concurrent.Map amplía la interfaz de actualización sobre la base de sync.Map. El objetivo de diseño de la interfaz de actualización es admitir la actualización simultánea del mismo objeto de valor. La actualización es diferente de la tienda. En la tienda, solo se escribirá el valor de una corrutina y las demás se sobrescribirán. La actualización puede garantizar que todas las actualizaciones de la rutina se actualicen en el objeto de valor, a menos que estas corrutinas actualicen el valor. Campos con el mismo objeto.

Debido a la explicación detallada de sync.Map en el capítulo anterior, será muy fácil entender el contenido ampliado de este capítulo La clave es que sync.Map en sí mismo tiene esta capacidad, nada más que ninguna interfaz especial se ha desarrollado.

// Update会持续调用tryUpdate()直到更新成功,因为过程中可能会冲突,就像CAS失败一样。每次更新前
// 都会将当前值通过tryUpdate传给调用者,调用者需要参考传入的value,因为其中可能包含已完成的更新,
// 调用者需要将更新的value通过tryUpdate返回,如果不需要更新则返回false
func (m *Map) Update(key interface{}, tryUpdate func(interface{}) (interface{}, bool)) bool {
    for {
        // load()函数实现与Load一样,只是返回值变成了*entry
        e, ok := m.load(key)
        if ok {
            // 即便entry存在,也可能value是空的亦或是expunged,这种entry也视为value不存在
            _, ok = e.load()
        }
        // 如果value不存在则用LoadOrStore()实现.
        if !ok {
            if value, ok := tryUpdate(nil); !ok {
                return false
            } else if _, loaded := m.LoadOrStore(key, value); !loaded {
                return true
            }
        // 如果value存在,让entry尝试更新
        } else if updated, ok := e.tryUpdate(tryUpdate); !ok {
            return false
        } else if updated {
            return true
        }
    }
}

func (e *entry) tryUpdate(tryUpdate func(interface{}) (interface{}, bool)) (bool, bool) {
    // 再次校验,因为可能被更新过
    p := atomic.LoadPointer(&e.p)
    if p == expunged || p == nil {
        return false, true
    }
    // 调用tryUpdate()获取更新的value
    value, ok := tryUpdate(*(*interface{})(p))
    if !ok {
        return false, false
    }
    for {
        // 当前值与调用tryUpdate()之前的值相同才能更新,否则会覆盖其他已完成的更新
        if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(&value)) {
            return true, true
        }
        p = atomic.LoadPointer(&e.p)
        if p == expunged || p == nil {
            return false, true
        }
        value, ok = tryUpdate(*(*interface{})(p))
        if !ok {
            return false, false
        }
    }
}

Ahora explicaré qué escenarios usará la interfaz de Actualización, como la función CAS en gmap, que es similar a la función de etcd, que es comparar la revisión del valor, y actualizarlo si es el mismo que el especificado revisión. El valor versionado almacenado en sync.Map se define de la siguiente manera:

type revisionedValue struct {
    Revision uint64
    Value    interface{}
}

Entonces, en este momento, concurrent.Map necesita poder obtener el valor revisado, comparar la revisión y actualizar el valor revisado de forma atómica. La función de devolución de llamada tryUpdate hace que Update sea muy flexible. Concurrent.Map pasa el valor actual a la persona que llama para decidir si actualizar o no. Se puede comparar (similar a CAS) u otros juicios.

Por supuesto, este requisito también se puede resolver aprendiendo la entrada abstracta sync.Map, es decir, * la entrada se almacena en sync.Map, y luego se entregan al usuario varias operaciones simultáneas en la entrada para que las realice. Esto definitivamente no es un problema. El objetivo del autor de extender sync.Map es implementar funciones similares en concurrent.Map, para que no tenga que implementarlo cada vez que lo use.

Además de Actualizar, el autor también amplió las interfaces Borrar y Copiar. Clear se usa para vaciar atómicamente el mapa, porque sync.Map es una estructura, y no es atómico al asignar un nuevo valor, excepto al usar el tipo * sync.Map y configurar atómicamente el puntero.

LEAQ 0(IP), CX          [3:7]R_PCREL:type.sync.Map
MOVQ CX, 0(SP)
MOVQ AX, 0x8(SP)
CALL 0x4be              [1:5]R_CALL:runtime.typedmemclr
以上是下面对sync.Map类型变量x赋值代码的汇编,证明赋值的方式是不原子的。
var x sync.Map
x = sync.Map{}

Debido a que sync.Map tiene muchos escenarios de uso que no son de puntero, Clear también tiene aplicaciones. El código es el siguiente:

func (m *Map) Clear() {
    // 实现很简单,就是清空read和dirty
    m.mu.Lock()
    m.read.Store(readOnly{})
    m.dirty, m.misses = nil, 0
    m.mu.Unlock()
}

La interfaz Copiar admite la copia atómica de un mapa en concurrent.Map, porque el tipo de mapa almacenado en concurrent.Map es la entrada map [interface {}] *, y el tipo de mapa de entrada no se puede predecir, por lo que la conversión de tipos es difícil de evitar. Para minimizar la sobrecarga, el autor define el tipo de Ranger, como se muestra en el siguiente código:

// Ranger主要功能是对输入map的遍历,这样只需要读取一次输入map,写入一次输出map,开销最小
type Ranger interface {
    Len() int
    Range(func(key, value interface{}) bool)
}

func (m *Map) Copy(r Ranger) {
    // 构造一个与输入map一样大小的map
    read := readOnly{m: make(map[interface{}]*entry, r.Len())}
    // 遍历输入map然后逐一插入到新map中
    r.Range(func(key, value interface{}) bool {
        read.m[key] = &entry{p: unsafe.Pointer(&value)}
        return true
    })
    // 最后将新的map写入read并清空dirty完成原子拷贝
    m.mu.Lock()
    m.read.Store(read)
    m.dirty, m.misses = nil, 0
    m.mu.Unlock()
}

En cuanto a la procedencia de los requisitos para Clear y Copy, por supuesto es gmap, porque el autor lo mencionó al principio, y sync.Map se amplió debido a los requisitos de gmap. Estas dos interfaces se utilizan principalmente para restaurar gmap desde una instantánea.

Supongo que te gusta

Origin blog.csdn.net/weixin_42663840/article/details/107958274
Recomendado
Clasificación