【Go进阶】怎么实现并发安全的map

这篇具有很好参考价值的文章主要介绍了【Go进阶】怎么实现并发安全的map。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

go语言提供的数据类型中,只有channel是并发安全的,基础map并不是并发安全的。以下三种方案实现了并发安全的map。

方案一:读写锁+map

实现原理:

给map添加一把读写锁,读操作加读锁进行读取;添加,更新,删除,遍历,获取长度这些操作加写锁后在进行操作。

代码实现:

以下代码是并发map的实现演示:

type RWMap struct {
    sync.RWMutex
    m map[any]any
}

func NewGRWMap() *RWMap {
    return &RWMap{
       m: make(map[any]any),
    }
}
func (m *RWMap) Get(k int) (any, bool) {
    m.RLock()
    defer m.RUnlock()
    v, existed := m.m[k]
    return v, existed
}

func (m *RWMap) Set(k any, v any) {
    m.Lock()
    defer m.Unlock()
    m.m[k] = v
}

func (m *RWMap) Delete(k any) {
    m.Lock()
    defer m.Unlock()
    delete(m.m, k)
}

func (m *RWMap) Len() int {
    m.RLock()
    defer m.RUnlock()
    return len(m.m)
}

func (m *RWMap) Each(f func(k, v any) bool) {
    m.RLock()
    defer m.RUnlock()

    for k, v := range m.m {
       if !f(k, v) {
          return
       }
    }
}

上述代码在读的时候加了个读锁,这个读锁在sync.RWMutex中并没有使用锁,只是将 readerCount这个字段+1。增删改是加了写锁,写锁在sync.RWMutex中每次都需要加锁。
以上可知,读写锁的加锁力度很大,当需要读多写少的情况下可以使用读写锁加map实现并发安全。

方案二:分片加锁

实现原理:

分片加锁的原理就如其名字一样,将一个map分成多个片段(一个片段是一个map),每个片段有自己的锁。

【Go进阶】怎么实现并发安全的map,golang,后端

代码实现:

concurrent-map提供了一种高性能的解决方案:通过对内部map进行分片,降低锁粒度,从而达到最少的锁等待时间(锁冲突)

以下是分片加锁中重要的数据类型的结构体:

var SHARD_COUNT = 32

type ConcurrentMap[K comparable, V any] struct {
	shards   []*ConcurrentMapShared[K, V]
	sharding func(key K) uint32
}

type ConcurrentMapShared[K comparable, V any] struct {
	items        map[K]V
	sync.RWMutex 
}

结构如下图所示:
【Go进阶】怎么实现并发安全的map,golang,后端

上述代码使用泛型实现了不同类型的map[comparable]any 。调用NewWithCustomShardingFunction函数,传入泛型的类型参数哈希函数,就可以得到一个并发安全的map了。

func create[K comparable, V any](sharding func(key K) uint32) ConcurrentMap[K, V] {
	m := ConcurrentMap[K, V]{
		sharding: sharding,
		shards:   make([]*ConcurrentMapShared[K, V], SHARD_COUNT),
	}
	for i := 0; i < SHARD_COUNT; i++ {
		m.shards[i] = &ConcurrentMapShared[K, V]{items: make(map[K]V)}
	}
	return m
}

func New[V any]() ConcurrentMap[string, V] {
	return create[string, V](fnv32)
}


func NewWithCustomShardingFunction[K comparable, V any](sharding func(key K) uint32) ConcurrentMap[K, V] {
    return create[K, V](sharding)
}

添加过程

【Go进阶】怎么实现并发安全的map,golang,后端

func (m ConcurrentMap[K, V]) Set(key K, value V) {
    shard := m.GetShard(key)
    shard.Lock()
    shard.items[key] = value
    shard.Unlock()
}

func (m ConcurrentMap[K, V]) GetShard(key K) *ConcurrentMapShared[K, V] {
	return m.shards[uint(m.sharding(key))%uint(SHARD_COUNT)]
}

大致流程如上图所示:

  1. 调用sharding得到哈希值。
  2. 对哈希值取模于切片长度,得到对应的分片map。
  3. 对map加写锁进行操作。

其他流程大致一样,大致都是先找“坑”,再进行读写操作。也可以将分片加锁方案简单的理解为是对读写加锁的方案的升级。

方案三:sync.Map

我们先看一下1.21版本的sync.Map的结构体:

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


type readOnly struct {
	m       map[any]*entry
	amended bool
}


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

这个结构的关系如下图所示:

【Go进阶】怎么实现并发安全的map,golang,后端

应对特殊场景的 sync.Map

在官方文档中指出,在以下两个场景中使用sync.Map,会比使用读写锁+map,的性能好:

  1. 只会增长的缓存系统中,一个key只写入一次而被读很多次;
  2. 多个goroutine为不相交的读,写和重写的键值对。

sync.Map的操作流程

读Load()
func (x *Pointer[T]) Load() *T { return (*T)(LoadPointer(&x.v)) }

func (m *Map) loadReadOnly() readOnly {
    if p := m.read.Load(); p != nil {
       return *p
    }
    return readOnly{}
}

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()
}

func (e *entry) load() (value any, ok bool) {
    p := e.p.Load()
    if p == nil || p == expunged {
       return nil, false
    }
    return *p, true
}

func (m *Map) missLocked() {
    m.misses++
    if m.misses < len(m.dirty) {
       return
    }
    m.read.Store(&readOnly{m: m.dirty})
    m.dirty = nil
    m.misses = 0
}

读流程:

先去read中读,有数据直接读取e.load()结束;没有加锁,去dirty中读,e换成dirty的,调用m.missLocked(),判断dirty是否存在这个key,不存在return nil, false;存在e.load().

!ok && read.amended这个判断是在read不存在key并且dirty存在read中没有的数据时为true。
m.missLocked()记录miss的次数,当miss的次数大于m.dirty的长度时将dirty数据给read,dirty清空,miss重置为0。

【Go进阶】怎么实现并发安全的map,golang,后端

写Store()

func (m *Map) Store(key, value any) {
    _, _ = m.Swap(key, value)
}

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
}


func (m *Map) dirtyLocked() {
	if m.dirty != nil {
		return
	}

	read := m.loadReadOnly()
	m.dirty = make(map[any]*entry, len(read.m))
	for k, e := range read.m {
		if !e.tryExpungeLocked() {
			m.dirty[k] = e
		}
	}
}

写的流程:

先去read看key是否存在;存在:如果key的value值为expunged,返回false,走dirty操作;否则,使用cas原子操作直接赋值,结束流程。

返回false,走dirty操作:先加锁,再走一次read,看是否存在key。

  1. read存在,使用e.unexpungeLocked()使用cas将entry设置为nil,若cas成功,将dirty中的entry设置为nil。使用cas设置value。
  2. read不存在,dirty存在,使用cas设置value。
  3. 以上都不满足(read,dirty都不存在),判断read中是否缺少数据,缺少时给dirty添加key-value;不缺少时调用m.dirtyLocked(),将read中的数据更新到dirty中,将其中删除的数据设置为expunged,之后将read的amended设置为true,最后给dirty添加key-value。

解锁,结束。

【Go进阶】怎么实现并发安全的map,golang,后端

删Delete()

func (m *Map) Delete(key any) {
    m.LoadAndDelete(key)
}

func (m *Map) LoadAndDelete(key any) (value any, loaded 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]
			delete(m.dirty, key)
			m.missLocked()
		}
		m.mu.Unlock()
	}
	if ok {
		return e.delete()
	}
	return nil, false
}

func (e *entry) delete() (value any, ok bool) {
	for {
		p := e.p.Load()
		if p == nil || p == expunged {
			return nil, false
		}
		if e.p.CompareAndSwap(p, nil) {
			return *p, true
		}
	}
}

删除流程:

先看read中是否存在,存在,直接调用e.delete()结束

不存在,且read中缺少数据,加锁,再次查看read,存在:解锁,调用e.delete()结束;不存在:删除dirty中的key,再调用m.missLocked(),解锁,若dirty中存在并删除了,还需要调用e.delete(),若dirty不存在key,return结束。

【Go进阶】怎么实现并发安全的map,golang,后端

遍历Range()

func (m *Map) Range(f func(key, value any) bool) {
    read := m.loadReadOnly()
    if read.amended {
       m.mu.Lock()
       read = m.loadReadOnly()
       if read.amended {
          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
       }
    }
}

遍历流程:
获取read,若read的数据全,遍历read,若数据不全,加锁,将dirty数据更新到read中,并将dirty值为nil,misses置0,再遍历read。

【Go进阶】怎么实现并发安全的map,golang,后端

sync.Map安全并发实现

sync.Map在实现并发问题的同时提升性能的几个优化:文章来源地址https://www.toymoban.com/news/detail-774944.html

  1. 用空间换时间,使用两个map,一个无锁,一个有锁,减少加锁对性能的影响。
  2. 优先从无锁的map中读取,更新,删除。
  3. 动态调整,miss次数多之后,将加锁map数据给无锁map。
  4. 延迟删除,删除一个键值只是进行了软删除,在动态调整时会进行硬删除。
  5. double check,访问有锁map,加锁后再次检查无锁map,是否有数据。

到了这里,关于【Go进阶】怎么实现并发安全的map的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Golang星辰图】加密和安全进阶:拓展Go的加密能力与身份验证

    在当今信息时代,保护用户数据和网络通信的安全至关重要。为了确保应用程序和用户之间的数据传输的机密性、完整性和真实性,加密和安全技术成为开发者必须掌握的关键领域之一。Go语言作为一种强大而灵活的编程语言,提供了丰富的加密和安全功能。本文将介绍一些

    2024年04月11日
    浏览(62)
  • Golang扫盲式学习——GO并发 | (一)

    并行:同一个时间段内多个任务同时在不同的CPU核心上执行。强调同一时刻多个任务之间的” 同时执行 “。 并发:同一个时间段内多个任务都在进展。强调多个任务间的” 交替执行 “。 随着硬件水平的提高,现在的终端主机都是多个CPU,每个CPU都是多核结构。当多个CPU同

    2024年02月07日
    浏览(45)
  • Go 语言为什么不支持并发读写 map?

    大家好,我是 frank ,「 Golang 语言开发栈」公众号作者。 01 介绍 在 Go 语言项目开发中,我们经常会使用哈希表 map ,它的时间复杂度是 O(1) ,Go 语言中的 map 使用开放寻址法避免哈希碰撞。 Go 语言中的 map 并非原子操作,不支持并发读写操作。 Go 官方认为 map 在大多数情况下

    2024年02月02日
    浏览(60)
  • Go后端开发 -- 数组 && slice && map && range

    go中的数组是固定长度的; 声明数组 Go 语言数组声明需要指定元素类型及元素个数,语法格式如下: 例如: 初始化数组 初始化数组中 {} 中的元素个数不能大于 [] 中的数字。 如果忽略 [] 中的数字不设置数组大小,Go 语言会根据元素的个数来设置数组的大小: 以下实例读取

    2024年01月17日
    浏览(48)
  • 使用go_concurrent_map 管理 并发更新缓存

    在后台服务中,为了提速,我在内存还做了一个告诉缓存来管理用户信息,根据更新通知,或者定时去redis中同步信息,那么在加载或者更新某个用户元素时,要防止并发, 当: 1)如果内存缓存没有; 2)去数据库或者redis加载; 3)添加到内存缓存; 这里就有个并发重复的

    2024年04月25日
    浏览(35)
  • 【Golang】Golang进阶系列教程--Go 语言数组和切片的区别

    在 Go 语言中,数组和切片看起来很像,但其实它们又有很多的不同之处,这篇文章就来说说它们到底有哪些不同。 数组和切片是两个常用的数据结构。它们都可以用于存储一组相同类型的元素,但在底层实现和使用方式上存在一些重要的区别。 Go 中数组的长度是不可改变的

    2024年02月15日
    浏览(61)
  • 【Golang】Golang进阶系列教程--Go 语言切片是如何扩容的?

    在 Go 语言中,有一个很常用的数据结构,那就是切片(Slice)。 切片是一个拥有相同类型元素的可变长度的序列,它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。 切片是一种引用类型,它有三个属性:指针,长度和容量。 底层源码定义如下: 指针: 指向

    2024年02月14日
    浏览(70)
  • Go 语言并发编程 及 进阶与依赖管理

    协程可以理解为 轻量级线程 ; Go更适 合高并发场景原因 之一: Go语言 一次可以创建上万协成 ; “快速”: 开多个协成 打印。 go func() : 在 函数前加 go 代表 创建协程 ; time.Sleep() : 协程阻塞,使主协程 在 子协程结束前阻塞不退出 ; 乱序输出 说明并行 ; 通过通信共享内

    2024年02月13日
    浏览(55)
  • golang并发安全-select

    前面说了golang的channel, 今天我们看看golang select 是怎么实现的。 select 非默认的case 中都是处理channel 的 接受和发送,所有scase 结构体中c是用来存储select 的case中使用的channel 编译器在中间代码生成期间会根据 select 中 case 的不同对控制语句进行优化,这一过程都发生在cmd/c

    2024年01月23日
    浏览(49)
  • golang--sync.map(安全字典)

    引言:在Go语言中,多个goroutine之间安全地共享数据是一项挑战。为了解决这个问题,Go语言提供了sync包,并在其中引入了sync.Map类型。sync.Map是一种并发安全的映射数据结构,它提供了高效的并发访问方式,避免了显式的锁操作。本文将深入探讨sync.Map的使用方法和底层实现

    2024年02月13日
    浏览(48)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包