首页 文章资讯内容详情

【GoLang】GoLang map 非线程安全 & 并发度写优化

2026-06-01 4 花语

本文内容纲要:

Catena(时序存储引擎)中有一个函数的实现备受争议,它从map中根据指定的name获取一个metricSource。每一次插入操作都会至少调用一次这个函数,现实场景中该函数调用更是频繁,并且是跨多个协程的,因此我们必须要考虑同步。

该函数从map[string]*metricSource中根据指定的name获取一个指向metricSource的指针,如果获取不到则创建一个并返回。其中要注意的关键点是我们只会对这个map进行插入操作。

简单实现如下:(为节省篇幅,省略了函数头和返回,只贴重要部分)

varsource*memorySource varpresentbool p.lock.Lock()//lockthemutex deferp.lock.Unlock()//unlockthemutexattheend ifsource,present=p.sources[name];!present{ //Thesourcewasntfound,sowellcreateit. source=&memorySource{ name:name, metrics:map[string]*memoryMetric{}, } //Insertthenewlycreated*memorySource. p.sources[name]=source }

经测试,该实现大约可以达到1,400,000插入/秒(通过协程并发调用,GOMAXPROCS设置为4)。看上去很快,但实际上它是慢于单个协程的,因为多个协程间存在锁竞争。

我们简化一下情况来说明这个问题,假设两个协程分别要获取“a”、“b”,并且“a”、“b”都已经存在于该map中。上述实现在运行时,一个协程获取到锁、拿指针、解锁、继续执行,此时另一个协程会被卡在获取锁。等待锁释放是非常耗时的,并且协程越多性能越差。

让它变快的方法之一是移除锁控制,并保证只有一个协程访问这个map。这个方法虽然简单,但没有伸缩性。下面我们看看另一种简单的方法,并保证了线程安全和伸缩性。

varsource*memorySource varpresentbool ifsource,present=p.sources[name];!present{//addedthisline //Thesourcewasntfound,sowellcreateit. p.lock.Lock()//lockthemutex deferp.lock.Unlock()//unlockattheend ifsource,present=p.sources[name];!present{ source=&memorySource{ name:name, metrics:map[string]*memoryMetric{}, } //Insertthenewlycreated*memorySource. p.sources[name]=source } //ifpresentistrue,thenanothergoroutinehasalreadyinserted //theelementwewant,andsourceissettowhatwewant. }//addedthisline //Notethatifthesourcewaspresent,weavoidthelockcompletely!

该实现可以达到5,500,000插入/秒,比第一个版本快3.93倍。有4个协程在跑测试,结果数值和预期是基本吻合的。

这个实现是ok的,因为我们没有删除、修改操作。在CPU缓存中的指针地址我们可以安全使用,不过要注意的是我们还是需要加锁。如果不加,某协程在创建插入source时另一个协程可能已经正在插入,它们会处于竞争状态。这个版本中我们只是在很少情况下加锁,所以性能提高了很多。

JohnPotocny建议移除defer,因为会延误解锁时间(要在整个函数返回时才解锁),下面给出一个“终极”版本:

varsource*memorySource varpresentbool ifsource,present=p.sources[name];!present{ //Thesourcewasntfound,sowellcreateit. p.lock.Lock()//lockthemutex ifsource,present=p.sources[name];!present{ source=&memorySource{ name:name, metrics:map[string]*memoryMetric{}, } //Insertthenewlycreated*memorySource. p.sources[name]=source } p.lock.Unlock()//unlockthemutex } //Notethatifthesourcewaspresent,weavoidthelockcompletely!

9,800,000插入/秒!改了4行提升到7倍啊!!有木有!!!!

更新:(译注:原作者循序渐进非常赞)

上面实现正确么?No!通过GoDataRaceDetector我们可以很轻松发现竟态条件,我们不能保证map在同时读写时的完整性。

下面给出不存在竟态条件、线程安全,应该算是“正确”的版本了。使用了RWMutex,读操作不会被锁,写操作保持同步。

varsource*memorySource varpresentbool p.lock.RLock() ifsource,present=p.sources[name];!present{ //Thesourcewasntfound,sowellcreateit. p.lock.RUnlock() p.lock.Lock() ifsource,present=p.sources[name];!present{ source=&memorySource{ name:name, metrics:map[string]*memoryMetric{}, } //Insertthenewlycreated*memorySource. p.sources[name]=source } p.lock.Unlock() }else{ p.lock.RUnlock() }

经测试,该版本性能为其之前版本的93.8%,在保证正确性的前提先能到达这样已经很不错了。也许我们可以认为它们之间根本没有可比性,因为之前的版本是错的。

参考资料:

Golang的锁和线程安全的Map:http://www.java123.net/404333.html

[Golang]Map的一个绝妙特性:http://studygolang.com/articles/2494

如何证明gomap不是并发安全的:https://segmentfault.com/q/1010000006259232

go语言映射map的线程协程安全问题:http://blog.csdn.net/htyu_0203_39/article/details/50979992

优化Go中的map并发存取:http://studygolang.com/articles/2775

扩展:

优化Go中的map并发存取|Go语言中文网|Golang中文社区|Golang中国

DataRaceDetector-TheGoProgrammingLanguage

golangmap安全_百度搜索

[Golang]Map的一个绝妙特性|Go语言中文网|Golang中文社区|Golang中国

Go语言map是怎么比较key是否存在的?-Go语言-知乎

Map线程安全几种实现方法-雲端之風-博客园

golang中map并发读写操作|Go语言中文网|Golang中文社区|Golang中国

go语言映射map的线程协程安全问题--博客频道-CSDN.NET

golang-如何证明gomap不是并发安全的-SegmentFault

GoCommonsPool发布以及Golang多线程编程问题总结-OPEN开发经验库

golangsync.RWMutex|Go语言中文网|Golang中文社区|Golang中国

[Golang]互斥到底该谁做?channel还是Mutex-Sunface-博客频道-CSDN.NET

golang中sync.RWMutex和sync.Mutex区别|Go语言中文网|Golang中文社区|Golang中国

GO语言并发编程之互斥锁、读写锁详解_Golang_脚本之家

go-HowtouseRWMutexinGolang?-StackOverflow

Golang同步:锁的使用案例详解-综合编程类其他综合-红黑联盟

golang读写锁RWMutex_Go语言_第七城市

本文内容总结:

原文链接:https://www.cnblogs.com/junneyang/p/6069981.html