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

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

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

var source *memorySource
var present bool p.lock.Lock() // lock the mutex
defer p.lock.Unlock() // unlock the mutex at the end if source, present = p.sources[name]; !present {
// The source wasn't found, so we'll create it.
source = &memorySource{
name: name,
metrics: map[string]*memoryMetric{},
} // Insert the newly created *memorySource.
p.sources[name] = source
}

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

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

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

var source *memorySource
var present bool if source, present = p.sources[name]; !present { // added this line
// The source wasn't found, so we'll create it. p.lock.Lock() // lock the mutex
defer p.lock.Unlock() // unlock at the end if source, present = p.sources[name]; !present {
source = &memorySource{
name: name,
metrics: map[string]*memoryMetric{},
} // Insert the newly created *memorySource.
p.sources[name] = source
}
// if present is true, then another goroutine has already inserted
// the element we want, and source is set to what we want. } // added this line // Note that if the source was present, we avoid the lock completely!

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

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

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

var source *memorySource
var present bool if source, present = p.sources[name]; !present {
// The source wasn't found, so we'll create it. p.lock.Lock() // lock the mutex
if source, present = p.sources[name]; !present {
source = &memorySource{
name: name,
metrics: map[string]*memoryMetric{},
} // Insert the newly created *memorySource.
p.sources[name] = source
}
p.lock.Unlock() // unlock the mutex
} // Note that if the source was present, we avoid the lock completely!

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


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

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

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

var source *memorySource
var present bool p.lock.RLock()
if source, present = p.sources[name]; !present {
// The source wasn't found, so we'll create it.
p.lock.RUnlock()
p.lock.Lock()
if source, present = p.sources[name]; !present {
source = &memorySource{
name: name,
metrics: map[string]*memoryMetric{},
} // Insert the newly created *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

如何证明 go map 不是并发安全的: 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中国
Data Race Detector - The Go Programming Language
golang map 安全_百度搜索
[Golang]Map的一个绝妙特性 | Go语言中文网 | Golang中文社区 | Golang中国
Go语言map是怎么比较key是否存在的? - Go 语言 - 知乎
Map线程安全几种实现方法 - 雲端之風 - 博客园
golang 中map并发读写操作 | Go语言中文网 | Golang中文社区 | Golang中国
go语言映射map的线程协程安全问题 - - 博客频道 - CSDN.NET
golang - 如何证明 go map 不是并发安全的 - SegmentFault
Go Commons Pool发布以及Golang多线程编程问题总结 - OPEN 开发经验库
golang sync.RWMutex | Go语言中文网 | Golang中文社区 | Golang中国
[Golang]互斥到底该谁做?channel还是Mutex - Sunface - 博客频道 - CSDN.NET
golang中sync.RWMutex和sync.Mutex区别 | Go语言中文网 | Golang中文社区 | Golang中国
GO语言并发编程之互斥锁、读写锁详解_Golang_脚本之家
go - How to use RWMutex in Golang? - Stack Overflow
Golang同步:锁的使用案例详解 - 综合编程类其他综合 - 红黑联盟
golang读写锁RWMutex_Go语言_第七城市

【GoLang】GoLang map 非线程安全 & 并发度写优化的更多相关文章

  1. UNIX环境高级编程——线程属性之并发度

    并发度控制着用户级线程可以映射的内核线程或进程的数目.如果操作系统的实现在内核级的线程和用户级的线程之间保持一对一的映射,那么改变并发度并不会有什么效果,因为所有的用户级线程都可能被调度到.但是,如果 ...

  2. Golang中map的三种声明方式和简单实现增删改查

    package main import ( "fmt" ) func main() { test3 := map[string]string{ "one": & ...

  3. 总结golang之map

    总结golang之map 2017年04月13日 23:35:53 趁年轻造起来 阅读数:18637 标签: golangmapgo 更多 个人分类: golang   版权声明:本文为博主原创文章, ...

  4. Golang入门(4):并发

    摘要 并发程序指同时进行多个任务的程序,随着硬件的发展,并发程序变得越来越重要.Web服务器会一次处理成千上万的请求,这也是并发的必要性之一.Golang的并发控制比起Java来说,简单了不少.在Go ...

  5. 数据结构和算法(Golang实现)(10)基础知识-算法复杂度主方法

    算法复杂度主方法 有时候,我们要评估一个算法的复杂度,但是算法被分散为几个递归的子问题,这样评估起来很难,有一个数学公式可以很快地评估出来. 一.复杂度主方法 主方法,也可以叫主定理.对于那些用分治法 ...

  6. golang 中 map 转 struct

    golang 中 map 转 struct package main import ( "fmt" "github.com/goinggo/mapstructure&qu ...

  7. golang之map的使用声明

    1.map的基本介绍 map是key-value数据结构,又称为字段或者关联数组.类似其它编程语言的集合,在编程中是经常使用到的 2.map的声明 1)基本语法 var map 变量名 map[key ...

  8. 数据结构和算法(Golang实现)(9)基础知识-算法复杂度及渐进符号

    算法复杂度及渐进符号 一.算法复杂度 首先每个程序运行过程中,都要占用一定的计算机资源,比如内存,磁盘等,这些是空间,计算过程中需要判断,循环执行某些逻辑,周而反复,这些是时间. 那么一个算法有多好, ...

  9. Java 非线程安全的HashMap如何在多线程中使用

    Java 非线程安全的HashMap如何在多线程中使用 HashMap 是非线程安全的.在多线程条件下,容易导致死循环,具体表现为CPU使用率100%.因此多线程环境下保证 HashMap 的线程安全 ...

随机推荐

  1. CSS3新增基础属性总结——20160409(易达客)

    1.box-shadow :h-shadow v-shadow blur spread color inset(outset) h-shadow:必须:水平阴影位置,允许负值. v-shadow:必须 ...

  2. 常用的JS HTML DOM 事件

    HTML DOM 事件 HTML DOM 事件允许Javascript在HTML文档元素中注册不同事件处理程序. 事件通常与函数结合使用,函数不会在事件发生前被执行! (如用户点击按钮). 提示: 在 ...

  3. Effective Objective-C 2.0 — 第一条:了解Objective-C语言的起源

    第一条: 了解Objective-C语言的起源 由Smalltalk演化而来,消息型语言的鼻祖(messaging structure)而非 (function calling)函数调用 //Mess ...

  4. Web框架们

    Python之路[第十八篇]:Web框架们   Python的WEB框架 Bottle Bottle是一个快速.简洁.轻量级的基于WSIG的微型Web框架,此框架只由一个 .py 文件,除了Pytho ...

  5. 异步编程中的最佳做法(async await)

    阅读1:http://blog.csdn.net/nacl025/article/details/9163495 阅读2:http://www.cnblogs.com/x-xk/archive/201 ...

  6. JBoss7 安装配置

    一.下载安装 1.下载地址: http://www.jboss.org/jbossas/downloads ,下载Certified Java EE 6 Full Profile版本. 2.解压 jb ...

  7. BeanNameAware接口和BeanFactoryAware接口

    迄今为止,所接触到的Bean都是“无知觉”的,就像黑客帝国中机械工厂里面“养殖”的人类,他们虽然能完成一定的功能,但是根本不知道自己在工厂(BeanFactory)中的代号(id),或者自己是在哪个工 ...

  8. jquery的$.extend、$.fn.extend、 jQuery.extend( target, object1, [objectN])作用及区别

    jQuery为开发插件提拱了两个方法,分别是: jQuery.fn.extend(); jQuery.extend(); 虽然 javascript 没有明确的类的概念,但是用类来理解它,会更方便. ...

  9. setTimeout和setInterval的各自使用场景

    默认的 setTimeout 只执行一次, 清除用clearTimeout setInterval 每间隔指定的时间, 就执行一次, 清除用clearInterval 但是, setTimeout也可 ...

  10. gitlab一键安装 (转)

    原文地址:http://www.2cto.com/os/201411/353292.html 0 简介bitnami和gitlab bitnami BitNami是一个开源项目,该项目产生的开源软件包 ...