设为首页 收藏本站
查看: 950|回复: 0

[经验分享] 【GoLang】GoLang map 非线程安全 & 并发度写优化

[复制链接]

尚未签到

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


运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-594317-1-1.html 上篇帖子: Golang 之协程详解 下篇帖子: golang并发编程的两种限速方法
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表