首页 > 其他分享 >Golang: 探究sync.map的实现

Golang: 探究sync.map的实现

时间:2024-03-23 22:22:21浏览次数:20  
标签:map Map read sync Golang dirty key entry

Golang: 探究sync.map的实现

背景

探究下载并发模式下, sync.map 的实现, 以及该实现方式可能引入的问题

链接

Github

基本使用

package main

import "sync"

func main() {
	m := sync.Map{}
	m.Load("key")
	m.Store("key", "value")

	m.LoadOrStore("key", "value")

	m.Delete("key")
}

以上就是主要的增删改查的能力.

数据结构

entry

type entry struct {
	p atomic.Pointer[any]
}

分析

sync​包的Map​类型中,entry​是一个结构体,它代表了Map​中的一个槽位,对应于一个特定的键。

entry​的主要作用是存储Map​中的键值对。它包含一个p​字段,这是一个原子指针,指向存储在条目中的值。p​字段的状态(是否为nil​或expunged​)决定了条目的状态(是否已被删除或有效)。

此外,entry​还提供了一些方法,如load​、tryCompareAndSwap​、unexpungeLocked​、swapLocked​、tryLoadOrStore​和delete​,这些方法用于操作存储在条目中的值,包括加载值、比较并交换值、取消标记删除、交换值、尝试加载或存储值以及删除值。这些操作都是并发安全的,因为它们使用了原子操作来处理p​字段。

Why atomic.Pointer?

atomic.Pointer​在Go语言中被用于实现并发安全的指针操作。

在多线程或者并发编程中,对共享数据的读写操作可能会发生在不同的goroutine中,这就可能导致数据的不一致性。为了解决这个问题,我们需要一种机制来保证在任何时刻,对共享数据的访问都只能有一个goroutine进行。

atomic.Pointer​提供了这样的机制。它提供了一组函数,可以对指针进行原子性的加载(Load)、存储(Store)和比较并交换(CompareAndSwap)操作。这些操作是并发安全的,也就是说,在多个goroutine同时进行这些操作时,不需要额外的锁机制就可以保证数据的一致性。

在你提供的代码中,p atomic.Pointer[any]​是entry​结构体的一个字段,它存储了对应的键值对的值。通过使用atomic.Pointer​,entry​可以在多个goroutine中安全地读写这个值,而不需要额外的锁。这对于sync.Map​这样的并发数据结构来说是非常重要的,因为它需要在多个goroutine中高效地读写数据。

readOnly

type readOnly struct {
	m       map[any]*entry
	amended bool // true if the dirty map contains some key not in m.
}

分析

readOnly​结构体有两个字段:

  • m​:这是一个存储entry​指针的map,其中entry​是Map​中的一个槽位,对应于一个特定的键。
  • amended​:这是一个布尔值,如果dirty​ map包含m​中没有的键,则为true​。

readOnly​的主要作用是存储Map​的一部分内容,这部分内容是可以安全进行并发访问的,无论是否持有mu​锁。readOnly​结构体的实例存储在Map​的read​字段中,可以通过原子操作进行加载和存储。

Map​的dirty​ map中存在read​ map中没有的键时,amended​字段会被设置为true​。这表示read​ map不再是最新的,需要通过复制dirty​ map来更新。这种设计可以帮助Map​在并发环境中高效地进行读写操作,减少锁的争用。

Map

type Map struct {
	mu sync.Mutex
	read atomic.Pointer[readOnly]
	dirty map[any]*entry
	misses int
}

分析

主要优化了两种常见的使用场景:(1) 键值对只写入一次但读取多次,如只增长的缓存;(2) 多个goroutine读写和覆盖不同键的条目。

sync.Map​的主要组成部分包括:

  1. mu​:一个互斥锁,用于保护对dirty​ map的访问。
  2. read​:一个原子指针,指向readOnly​结构体的实例。readOnly​结构体包含一个entry​指针的map和一个amended​布尔值。read​字段中的内容可以在不持有mu​锁的情况下并发访问。
  3. dirty​:一个存储entry​指针的map,需要持有mu​锁才能访问。dirty​ map包含了所有需要更新的键值对,以及read​ map中的所有非删除条目。
  4. misses​:一个计数器,记录自上次更新read​ map以来需要锁定mu​来确定键是否存在的加载操作的次数。

方法分析

Load

func (m *Map) Load(key any) (value any, ok bool) {
	read := m.loadReadOnly()
	e, ok := read.m[key]
	if !ok && read.amended {
		m.mu.Lock()
		read = m.loadReadOnly()
		e, ok = read.m[key]
		if !ok && read.amended {
			e, ok = m.dirty[key]
			m.missLocked()
		}
		m.mu.Unlock()
	}
	if !ok {
		return nil, false
	}
	return e.load()
}

代码分析

  1. 获取read map
  2. 如果read中存在, 则返回
  3. 如果read中不存在, 且read已经过时, 则尝试从dirty中获取, 同时 miss + 1

Store

func (m *Map) Swap(key, value any) (previous any, loaded bool) {
	read := m.loadReadOnly()
	if e, ok := read.m[key]; ok {
		if v, ok := e.trySwap(&value); ok {
			if v == nil {
				return nil, false
			}
			return *v, true
		}
	}

	m.mu.Lock()
	read = m.loadReadOnly()
	if e, ok := read.m[key]; ok {
		if e.unexpungeLocked() {
			// The entry was previously expunged, which implies that there is a
			// non-nil dirty map and this entry is not in it.
			m.dirty[key] = e
		}
		if v := e.swapLocked(&value); v != nil {
			loaded = true
			previous = *v
		}
	} else if e, ok := m.dirty[key]; ok {
		if v := e.swapLocked(&value); v != nil {
			loaded = true
			previous = *v
		}
	} else {
		if !read.amended {
			// We're adding the first new key to the dirty map.
			// Make sure it is allocated and mark the read-only map as incomplete.
			m.dirtyLocked()
			m.read.Store(&readOnly{m: read.m, amended: true})
		}
		m.dirty[key] = newEntry(value)
	}
	m.mu.Unlock()
	return previous, loaded
}

分析

Swap​方法是sync.Map​中的一个方法,它用于交换键对应的值,并返回之前的值(如果存在)。如果键在Map​中存在,那么loaded​返回值为true​,否则为false​。

Swap​方法的实现主要分为两个步骤:

  1. 首先,它会尝试在只读的read​ map中查找键对应的条目。如果找到了,并且可以成功地交换值,那么就直接返回。这是一种优化,可以避免在大多数情况下都需要获取锁。
  2. 如果在read​ map中没有找到键,或者无法交换值,那么就需要获取mu​锁,并在dirty​ map中查找和操作键对应的条目。在这个过程中,可能需要将dirty​ map提升为read​ map,或者在dirty​ map中添加新的条目。

这个方法首先尝试在read​ map中查找键对应的条目,并尝试交换值。如果成功,就直接返回。如果在read​ map中没有找到键,或者无法交换值,那么就需要获取mu​锁.

并在dirty​ map中查找和操作键对应的条目。在这个过程中,可能需要将dirty​ map提升为read​ map,或者在dirty​ map中添加新的条目。

注意

expunged 与 nil

区别

  1. 如果一个entry的p为 expunged, 表示一定发生了一次 dirty的初始化, 此时 entry 的 key 一定不在dirty中

  2. 如果一个entry的p为nil, 依然表示清除, 但是此时dirty中必然有同样的key

作用

所以, 对于已删除的key来说, 不能在expunged​的状态下进行更新操作, 因为这会导致在dirty提升为read时, 出现数据不一致的问题.

这就是, trySwap​的设计:

func (e *entry) trySwap(i *any) (*any, bool) {
	for {
		p := e.p.Load()
		if p == expunged {
			// 返回nil, false
			return nil, false
		}
		// 如果entry不为nil
		if e.p.CompareAndSwap(p, i) {
			// 返回entry的值, true
			return p, true
		}
	}
}

unexpungeLocked

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

为什么要使用 unexpungeLocked​我们可以考虑如果不使用unexpungeLocked​将expunged转为nil, 会发生什么?

那么我们在此时的使用就会变成

		if e.p.Load() == expunged {
			m.dirty[key] = e
		}

即虽然我们保证了 read 和 dirty中都存在同一个key, 但是此时的 e.p​都为 unexpungeLocked​, 无法调用封装的函数进行修改, 而只能直接使用e.p.Store(&i)​进行修改了,

这样会有什么问题?

  1. 原子操作的预期行为sync.Map​ 中的原子操作依赖于 expunged​ 和 nil​ 状态来决定条目是否可以被更新。如果一个条目是 expunged​ 状态,它不能直接被更新,因为这意味着该条目已经从 dirty​ 映射中删除了。如果不先将其置为 nil​,那么原子操作的预期行为就会被破坏。
  2. 状态一致性sync.Map​ 维护了 read​ 和 dirty​ 映射之间的一致性。条目从 expunged​ 状态恢复为 nil​ 是一个信号,表明该条目现在可以安全地更新,并且这个更新应该反映在 dirty​ 映射中。
  3. 并发安全:在并发环境中,其他 goroutines 可能会尝试读取或更新同一个键的条目。如果一个条目从 expunged​ 状态直接更新为一个新值,其他 goroutines 可能会遇到一个不一致的状态,因为它们预期要么是 expunged​ 要么是 nil​,然后才是一个有效的值。

因此,正确地处理 expunged​ 状态是保证 sync.Map​ 正确行为的关键。在更新一个条目之前,如果它被标记为 expunged​,应当首先通过 unexpungeLocked​ 方法将其状态置为 nil​,然后才能安全地进行更新。这样可以确保 sync.Map​ 的并发安全性和性能优化得到保持。

总结

  1. sync.Map目前的实现, 非常适合 读多写少的操作. 尤其是有大量非覆盖写的调用, 性能会直接回退到 map​+ mutex​(在覆盖写的情况下, read 和 dirty 可以共享entry, 实现乐观锁的更新, 性能会好一些.
  2. 在研究其代码时, 需要非常注意 nil​ 以及 expunged​两种状态的区别.

标签:map,Map,read,sync,Golang,dirty,key,entry
From: https://www.cnblogs.com/pDJJq/p/18091799/golang-explore-the-implementation-of-syncmap-1ixv

相关文章

  • web前端之node读取文件夹名称及html文件的标题、文件系统、路径处理、模块、正则、isD
    MENU代码解析代码constfs=require('fs');constpath=require('path');//文件夹路径//C:\mssj\web\web-case\case\nodeJs\index.js//C:\mssj\web\web-case\case\nodeJs\index.html//C:\mssj\web\web-case\case\ajaxProgressMoni......
  • HashMap的数组最大容量为什么要设计为2的30次方?而不是2的31次方-1?数组容量为什么一定
    目录问题 数组容量为什么一定要设计为2的幂(2的n次方)?1、首先要清楚HashMap的底层基本原理2、再来看下怎么通过hash值决定存放在哪个桶中?首先看下hash值再看下怎么确定当前key存放在哪个数组下标下的为什么要做按位与而不用模运算符%?为什么要n-1呢?n是一个什么样的数......
  • Golang标准库fmt深入解析与应用技巧
    Golang标准库fmt深入解析与应用技巧前言fmt包的基本使用打印与格式化输出函数Print系列函数格式化字符串格式化输入函数小结字符串格式化基本类型的格式化输出自定义类型的格式化输出控制格式化输出的宽度和精度小结错误处理与fmt使用fmt.Errorf生成错误信息fmt包与错......
  • 一文彻底搞懂HashMap
    文章目录1.数据结构2.扩容机制3.常问问题3.1HashMap为什么要树化3.2链表中转红黑树的阈值为什么设为81.数据结构JDK7中的HashMap使⽤的是数组+链表的实现⽅式,即拉链法。当发生哈希冲突时,即多个键映射到同一个数组索引位置时,HashMap会将这些键值对存储在......
  • golang interface转int
    varres[]orm.Params//切片map,value为interface;//获取当前时间now:=time.Now()//获取30天前的时间thirtyDaysAgo:=now.AddDate(0,0,-30)//将时间转换为时间戳timestamp:=thirtyDaysAgo.Unix()sql:=fmt.Sprint("SELECTcount(*)asnum,media.idasarticle_id,m......
  • Golang: Redislock源码分析
    Golang:Redislock源码分析源码https://github.com/bsm/redislock实现Lua脚本obtain.lua--obtain.lua:arguments=>[value,tokenLen,ttl]--Obtain.luatrytosetprovidedkeys'swithvalueandttliftheydonotexists.--Keyscanbeoverrideniftheyal......
  • P5266 【深基17.例6】学籍管理(Map)
    #include<bits/stdc++.h>usingnamespacestd;map<string,string>m;intmain(){ intn; cin>>n; while(n--) { inta; stringname,score; cin>>a; if(a==1) { cin>>name>>score; if(m.find(name)!=m.end())m[......
  • https://github.com/google/adb-sync
    大致的实现方式:是一个python文件,在windows上就pythonadb-sync-R-t-n--dry-run/storage/emulated/0C:\a\b这样运行 其中本机系统的文件列表和修改时间获取就用os库(importos)手机上的文件列表和修改时间获取就用ls-al     https://blog.csdn.net/chabb/ar......
  • 游戏开发:移植golang共享库 for lua
    一些奇奇怪怪的尝试:)随笔记录下将golang模块导出为共享库供lua使用(一般用于功能模块适配和迁移),这里给出一个借助c语言实现中间层通信的方案(不要问我为什么不使用ffi)。假设使用go实现底层模块core,export相关API(如下例的G_Signature)供外部使用,这里是被C层使用。那么需要将go模块编......
  • 是时候来唠一唠synchronized关键字了,Java多线程的必问考点!
    写在开头在之前的博文中,我们介绍了volatile关键字,Java中的锁以及锁的分类,今天我们花5分钟时间,一起学习一下另一个关键字:synchronized。synchronized是什么?首先synchronized是Java中的一个关键字,所谓关键字,就是Java中根据底层封装所赋予的一种具有特殊语义的单词,而synchronized......