Go实现map并发安全的3种方式总结

 更新时间:2023年10月08日 11:14:51   作者:好像、下雨了  
Go的原生map不是并发安全的,在多协程读写同一个map的时候,安全性无法得到保障,这篇文章主要给大家总结介绍了关于Go实现map并发安全的3种方式,需要的朋友可以参考下

实现map并发读写线程安全

1. 加锁

对整个map加上读写锁sync.RWMutex

//keyType为key的类型,valueType为value的类型
type RWMap struct {
	Map map[keyType]valueType
	sync.RWMutex
}
func NewRWMap(capacity int) *RWMap {
	if capacity < 0 {
		capacity = 0
	}
	return &RWMap{
		Map: make(map[keyType]valueType, capacity),
	}
}
//add or update
func (m *RWMap) Set(key keyType, value valueType) {
	m.Lock()
	defer m.Unlock()
	m.Map[key] = value
}
//delete
func (m *RWMap) Delete(key int)  {
	m.Lock()
	defer m.Unlock()
	delete(m.Map, key)
}
//get
func (m *RWMap) Get(key int) valueType {
	m.RLock()
	defer m.RUnlock()
	return m.Map[key]
}

优点:解决了问题。

缺点:锁粒度大。

2. 分片加锁

一个操作会导致整个map被锁住,导致性能降低。所以提出了分片思想,将一个map分成几个片,按片加锁。

第三方包实现:github.com/orcaman/concurrent-map

github上用 map language:go 搜索:

3.4kstar

插曲:注意,如果你的goland ide 版本太老的话,github.com/orcaman/concurrent-map/v2 版本是用不了的:

所以我最后换成VSCode,发现就没这个问题了。(因为新版本的GoLand还得继续想法子破解)

源码New方法返回的map,看到key只支持string

// Creates a new concurrent map.
func New[V any]() ConcurrentMap[string, V] {
	return create[string, V](fnv32)
}

Example and usage

package main
import (
	"fmt"
	"time"
	cmap "github.com/orcaman/concurrent-map/v2"
)
func main() {
	m := cmap.New[int]()
	for i := 0; i < 300; i++ {
		go func(i int) {
			m.Set(fmt.Sprintf("%v", i), i*2)  //并发写
		}(i)
	}
	time.Sleep(4 * time.Second)
	fmt.Println(len(m.Keys()))
}

execute and output:

PS C:\GoWork\src\asset-manager\mytest> go run main.go
300

并发写没问题。

更多使用示例包里的concurrent_map_test.go里面提供了。

3. sync.Map

标准库中的 sync.Map是专为 append-only 场景设计的。

sync.Map在读多写少性能比较好,否则并发性能很差。

Go源码:

// Map is like a Go map[interface{}]interface{} but is safe for concurrent use
// by multiple goroutines without additional locking or coordination. 
// Loads, stores, and deletes run in amortized constant time. 
//=====自注释======
sync.Map 很像Go map[interface{}]interface{}。但sync.Map是线程安全的,能被多个协程在没有额外的锁或者协调的情况下并发使用。
Loads,stores,deletes操作都运行在分摊常数时间内。
amortized 平摊的(adj.)英  /əˈmɔːtaɪzd/
//=====自注释======
//
// The Map type is specialized. Most code should use a plain Go map instead,
// with separate locking or coordination, for better type safety and to make it
// easier to maintain other invariants along with the map content.
//=====自注释======
invariants (n.) 不变量(invariant的复数)/ɪnˈveriənts/
sync.Map 类型是为特殊情况专门设计的。
大多数代码都应该使用普通的Go map + 单独的锁或者协调 ,这种形式,来获得更好的类型安全
以及使得在维护映射内容的同时维护其他不变量更容易。
//=====自注释======
//
// The Map type is optimized for two common use cases: (1) when the entry for a given
// key is only ever written once but read many times, as in caches that only grow,
// or (2) when multiple goroutines read, write, and overwrite entries for disjoint
// sets of keys. In these two cases, use of a Map may significantly reduce lock
// contention compared to a Go map paired with a separate Mutex or RWMutex.
//=====自注释======
disjoint (adj.) 不连贯的,(两个集合)不相交的 /dɪsˈdʒɔɪnt/
sync.Map类型针对两个常见用例进行了优化:
(1)对于一个给定的key,只会写一次,但是读很多次,就像在只增长的缓存中一样。
(2)当多个协程读,写,重写不相交的keys。
以上两种情况,相比于使用Go map + Mutex(或者RWMutex),使用sync.Map能显著减少锁竞争。
//=====自注释======
//
// The zero Map is empty and ready for use. A Map must not be copied after first use.
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 (adj.)/ɪkˈspʌndʒ/ 被擦去的,被删掉的
dirty map 包含map内容的部分,该部分要求持有mu锁。为了确保dirty map能快速提升到read map,
它还包括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[any]*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
}

read atomic.Value

sync/stomic包里都是go提供的原子操作。

sync.Map思想:就是用两个数据结构(只读的 read 和可写的 dirty)尽量将读写操作分开,并最小粒度加锁,来减少锁对性能的影响。

总结

较常使用的是前两种:加读写锁和分片加锁。特定场景下sync.Map性能会有更优的表现(要满足那两个场景条件比较苛刻,实际很少用)。

相关文章

  • 在Visual Studio Code中配置GO开发环境的详细教程

    在Visual Studio Code中配置GO开发环境的详细教程

    这篇文章主要介绍了在Visual Studio Code中配置GO开发环境的详细教程,需要的朋友可以参考下
    2017-02-02
  • Go日志之日志库讲解

    Go日志之日志库讲解

    本文介绍了Go语言中最优秀的日志库,包括Zap、log/slog、Logrus和Zerolog,它们都提供了结构化日志记录、自定义日志级别、输出目标等特性,感兴趣的可以了解一下
    2026-04-04
  • Go语言入门学习之Channel通道详解

    Go语言入门学习之Channel通道详解

    go routine可以使用channel来进行通信,使用通信的手段来共享内存,下面这篇文章主要给大家介绍了关于Go语言入门学习之Channel通道的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考下
    2022-07-07
  • 使用Golang实现AES加解密的代码示例

    使用Golang实现AES加解密的代码示例

    在现代的数据安全中,加密和解密是极其重要的一环,其中,高级加密标准(AES)是最广泛使用的加密算法之一,本文将介绍如何使用Golang来实现AES加密和解密,需要的朋友可以参考下
    2024-04-04
  • 基于go手动写个转发代理服务的代码实现

    基于go手动写个转发代理服务的代码实现

    这篇文章主要介绍了基于go手动写个转发代理服务的代码实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-02-02
  • 详解如何使用Golang扩展Envoy

    详解如何使用Golang扩展Envoy

    这篇文章主要为大家介绍了详解如何使用Golang扩展Envoy实现示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-06-06
  • 一文带你熟悉Go语言中的分支结构

    一文带你熟悉Go语言中的分支结构

    这篇文章主要和大家分享一下Go语言中的分支结构(if - else-if - else、switch),文中的示例代码讲解详细,对我们学习Go语言有一定的帮助,需要的可以参考一下
    2022-11-11
  • Go Map并发冲突预防与解决

    Go Map并发冲突预防与解决

    这篇文章主要为大家介绍了Go Map并发冲突预防与解决,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-12-12
  • 一文彻底理解Golang闭包实现原理

    一文彻底理解Golang闭包实现原理

    闭包对于一个长期写Java的开发者来说估计鲜有耳闻,光这名字感觉就有点"神秘莫测"。这篇文章的主要目的就是从编译器的角度来分析闭包,彻底搞懂闭包的实现原理,需要的可以参考一下
    2022-10-10
  • 深入了解Go的interface{}底层原理实现

    深入了解Go的interface{}底层原理实现

    本文主要介绍了Go的interface{}底层原理实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-06-06

最新评论