golang中sync.Map并发创建、读取问题实战记录

背景:

我们有一个用go做的项目,其中用到了zmq4进行通信,一个简单的rpc过程,早期远端是使用一个map去做ip和具体socket的映射。

问题

大概是这样

struct SocketMap {
 sync.Mutex
 sockets map[string]*zmq4.Socket
}

然后调用的时候的代码大概就是这样的:

func (pushList *SocketMap) push(ip string, data []byte) {
 pushList.Lock()
 defer pushList.UnLock()
 socket := pushList.sockets[string]
 if socket == nil {
 socket := zmq4.NewSocket()
 //do some initial operation like connect
 pushList.sockets[ip] = socket
 }
 socket.Send(data)
}

相信大家都能看出问题:当push被并发访问的时候(事实上push会经常被并发访问),由于这把大锁的存在,同时只能有一个协程在临界区工作,效率是会被大大降低的。

解决方案:会带来crash的优化

所以我们决定使用sync.Map来替代这个设计,然后出了第一版代码,写的非常简单,只做了简单的替换:

struct SocketMap {
 sockets sync.Map
}

func (pushList *SocketMap) push(ip string, data []byte) {
 var socket *zmq4.Socket
 socketInter, ok = pushList.sockets.Load(ip)
 if !ok {
 socket = zmq4.NewSocket()
 //do some initial operation like connect
 pushList.sockets.Store(ip, socket)
 } else {
 socket = socketInter.(*zmq4.Socket)
 }
 socket.Send(data)
}

乍一看似乎没什么问题?但是跑起来总是爆炸,然后一看log,提示有个非法地址。后来在github上才看到,zmq4.Socket不是线程安全的。上面的代码恰恰会造成多个线程同时拿到socket实例,然后就crash了。

解决方案2: 加一把锁也挡不住的冲突

然后怎么办呢?看来也只能加锁了,不过这次加锁不能加到整个map上,否则还会有性能问题,那就考虑减小锁的粒度吧,使用锁包装socket。这个时候我们的代码也就呼之欲出了:

struct SocketMutex{
 sync.Mutex
 socket *zmq4.Socket
}
struct SocketMap {
 sockets sync.Map
}

func (pushList *SocketMap) push(ip string, data []byte) {
 var socket *SocketMutex
 socketInter, ok = pushList.sockets.Load(ip)
 if !ok {
 socket = //do some initial operation like connect
 pushList.sockets.Store(ip, newSocket)
 } else {
 socket = socketInter.(*SocketMutex)
 }
 socket.Lock()
 defer socket.Unlock()
 socket.socket.Send(data)
}

但是这样还是有问题,相信经验比较丰富的老哥一眼就能看出来,问题处在socketInter, ok = pushList.sockets.Load(ip)这行代码上,如果map中没有这个值,且有多个协程同时访问到这行代码,显然这几个协程的ok都会置为false,然后都进入第一个if代码块,创建多个socket实例,并且争相覆盖原有值。

单纯解决这个问题也很简单,就是使用sync.Map.LoadOrStore(key interface{}, value interface{}) (v interface{}, loaded bool)这个api,来原子地去做读写。

然而这还没完,我们的写入新值的操作不光是调用一个api创建socket就完了,还要有一系列的初始化操作,我们必须保证在初始化完成之前,其他通过Load拿到这个实例的协程无法真正访问socket实例。

这时候显然sync.Map自带的机制已经无法解决这个问题了,那么我们必须寻求其他的手段,要么锁,要么就sync.WaitGroup或者whatever的其他什么东西。

解决方案3: 闭包带来的神奇体验

后来经大佬指点,我在encoder.go中看到了这么一段代码:

 func typeEncoder(t reflect.Type) encoderFunc {
 if fi, ok := encoderCache.Load(t); ok {
  return fi.(encoderFunc)
 }          

 // To deal with recursive types, populate the map with an
 // indirect func before we build it. This type waits on the
 // real func (f) to be ready and then calls it. This indirect
 // func is only used for recursive types.
 var (
  wg sync.WaitGroup
  f encoderFunc
 )
 wg.Add(1)
 fi, loaded := encoderCache.LoadOrStore(t, encoderFunc(func(e *encodeState, v reflect.Value, opts encOpts) {
  wg.Wait()
  f(e, v, opts)
 }))
 if loaded {
  return fi.(encoderFunc)
 }          

 // Compute the real encoder and replace the indirect func with it.
 f = newTypeEncoder(t, true)
 wg.Done()
 encoderCache.Store(t, f)
 return f
 }  

豁然开朗,我们可以在sync.Map中存放一个闭包函数,然后在闭包函数中等待本地的sync.WaitGroup完成再返回实例。于是最终的代码也就成型了。

struct SocketMutex{
 sync.Mutex
 socket *zmq4.Socket
}
struct SocketMap {
 sockets sync.Map
}

func (pushList *SocketMap) push(ip string, data []byte) {
 type SocketFunc func()*SocketMutex
 var (
  socket *SocketMutex
  w sync.WaitGroup
 )
 socket = &SocketMutex {
  socket : zmq4.NewSocket()
 }
 w.Add(1)
 socketf, ok = pushList.sockets.LoadOrStore(ip, SocketFunc(func()*SocketMutex) {
  w.Wait()
  return socket
 })
 if !ok {
  socket = //do some initial operation like connect
  w.Done()
 } else {
  socket = socketInter.(*SockeFunc)()
 }
 socket.Lock()
 defer socket.Unlock()
 socket.socket.Send(data)
}

总结:

并发代码中的竞争问题,每一行代码的重入性都要深思熟虑啊。

总的来说要保持以下几个准则:

(1) 不可重入访问的系统资源,如socketfd, filefd,signalfd(事实上大多数这种系统资源都是不可重入的)等,在使用无锁结构的容器、读写锁封装的容器时,需要给每个资源单独加锁或者使用其他手段保证系统资源在临界区受到有效保护。

(2)如果有读取,如果为空则写入的逻辑,需要使用能提供原子性保证的LoadOrSave调用,或者没有的话,自己实现也要保证读取和写入过程整体的原子性;防止并发访问Load调用时,多个线程都返回否而创建多个实例,然后在Save的时候又互相覆盖。——这个原则不光对成员是系统资源的时候生效,如果存放的是其他东西也同样适用。

(3)如果资源创建完毕,还需要其他的初始化过程,则可以考虑在容器内放置闭包,初始化过程使用sync.WaitGroup保护,在闭包中调用Wait方法等待初始化完成再给其他线程返回初始化好的实例。而初始化过程完成后,可以置换闭包函数,不再调用Wait方法,来减少可能的开销。

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • golang实现并发数控制的方法

    golang并发 谈到golang这门语言,很自然的想起了他的的并发goroutine.这也是这门语言引以为豪的功能点.并发处理,在某种程度上,可以提高我们对机器的使用率,提升系统业务处理能力.但是并不是并发量越大越好,太大了,硬件环境就会吃不消,反而会影响到系统整体性能,甚至奔溃.所以,在使用golang提供便捷的goroutine时,既要能够实现开启并发,也要学会如果控制并发量. 开启golang并发 golang开启并发处理非常简单,只需要在调用函数时,在函数前边添加上go关键字即可.如下

  • 如何利用Golang写出高并发代码详解

    前言 之前一直对Golang如何处理高并发http请求的一头雾水,这几天也查了很多相关博客,似懂非懂,不知道具体代码怎么写 下午偶然在开发者头条APP上看到一篇国外技术人员的一篇文章用Golang处理每分钟百万级请求,看完文章中的代码,自己写了一遍代码,下面自己写下自己的体会 核心要点 将请求放入队列,通过一定数量(例如CPU核心数)goroutine组成一个worker池(pool),workder池中的worker读取队列执行任务 实例代码 以下代码笔者根据自己的理解进行了简化,主要是表达出

  • golang高并发的深入理解

    前言 GO语言在WEB开发领域中的使用越来越广泛,Hired 发布的<2019 软件工程师状态>报告中指出,具有 Go 经验的候选人是迄今为止最具吸引力的.平均每位求职者会收到9 份面试邀请. 想学习go,最基础的就要理解go是怎么做到高并发的. 那么什么是高并发? 高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求. 严格意义上说,单核的CPU是没法做到并行的,只有多核的CPU才能做到严格意义上的并行

  • 详解Golang 中的并发限制与超时控制

    前言 上回在 用 Go 写一个轻量级的 ssh 批量操作工具里提及过,我们做 Golang 并发的时候要对并发进行限制,对 goroutine 的执行要有超时控制.那会没有细说,这里展开讨论一下. 以下示例代码全部可以直接在 The Go Playground上运行测试: 并发 我们先来跑一个简单的并发看看 package main import ( "fmt" "time" ) func run(task_id, sleeptime int, ch chan st

  • golang基础之Gocurrency并发

    goroutine只是由官方实现的超级"线程池"而已,每个实例4-5kb的栈内存占用和用于实现机制而大幅减少的创建和销毁开销. 并发不是并行(多CPU):  Concurrency Is Not Parallelism 并发主要由切换时间片来实现"同时"运行,并行则是直接利用多核实现多线程的运行,但Go可以设置使用核数,以发挥多核计算机的能力. 通过go关键字实现多线程 package main import ( "fmt" "time

  • Golang极简入门教程(三):并发支持

    Golang 运行时(runtime)管理了一种轻量级线程,被叫做 goroutine.创建数十万级的 goroutine 是没有问题的.范例: 复制代码 代码如下: package main   import (     "fmt"     "time" )   func say(s string) {     for i := 0; i < 5; i++ {         time.Sleep(100 * time.Millisecond)       

  • golang中sync.Map并发创建、读取问题实战记录

    背景: 我们有一个用go做的项目,其中用到了zmq4进行通信,一个简单的rpc过程,早期远端是使用一个map去做ip和具体socket的映射. 问题 大概是这样 struct SocketMap { sync.Mutex sockets map[string]*zmq4.Socket } 然后调用的时候的代码大概就是这样的: func (pushList *SocketMap) push(ip string, data []byte) { pushList.Lock() defer pushLi

  • 示例剖析golang中的CSP并发模型

    目录 1. 相关概念: 2. CSP (通信顺序进程) 3. channel:同步&传递消息 4. goroutine:实际并发执行的实体 5. golang调度器 1. 相关概念: 用户态:当一个进程在执行用户自己的代码时处于用户运行态(用户态) 内核态:当一个进程因为系统调用陷入内核代码中执行时处于内核运行态(内核态),引入内核态防止用户态的程序随意的操作内核地址空间,具有一定的安全保护作用.这种保护模式是通过内存页表操作等机制,保证进程间的地址空间不会相互冲突,一个进程的操作不会修改另一个

  • golang中sync.Mutex的实现方法

    目录 mutex 的实现思想 golang 中 mutex 的实现思想 mutex 的结构以及一些 const 常量值 Mutex 没有被锁住,第一个协程来拿锁 Mutex 仅被协程 A 锁住,没有其他协程抢锁,协程 A 释放锁 Mutex 已经被协程 A 锁住,协程 B 来拿锁 lockSlow() runtime_doSpin() runtime_canSpin() Mutex 被协程 A 锁住,协程 B 来抢锁但失败被放入等待队列,此时协程 A 释放锁 unlockSlow() Mutex

  • python中mediapipe库踩过的坑实战记录

    目录 bug1 解决(1): 解决(2): bug2 bug3 总结 bug1 无法正常使用cmd或pycharm正常安装,报错截图如下: 解决(1): 这种情况下,我们就不能使用cmd或pycharm进行安装了(若继续使用,则可以使用国内镜像进行加速安装,但是python中的一些高级库,国内镜像的文件是不全的,下载容易出问题!) 当然随着时间国内镜像版本的迭代,尝试国内镜像直接安装也是可以试一试的! 解决(2): 我们可以不使用cmd或pycharm进行自动安装,我们可以手动安装: 1.找到p

  • 关于golang中map使用的几点注意事项总结(强烈推荐!)

    目录 前言 1 使用 map 记得初始化 2 map 的遍历是无序的 3 map 也可以是二维的 4 获取 map 的 key 最好使用这种方式 5 map 是并发不安全的 ,sync.Map 才是安全的 总结 前言 日常的开发工作中,map 这个数据结构相信大家并不陌生,在 golang 里面,当然也有 map 这种类型 关于 map 的使用,还是有蛮多注意事项的,如果不清楚,这些事项,关键时候可能会踩坑,我们一起来演练一下吧 1 使用 map 记得初始化 写一个 demo 定义一个 map[

  • Golang中Map按照Value大小排序的方法实例

    目录 起因 探索 实现 第一步 第二步 第三步 总结 总结 Golang中的 map 默认是 无序的 . 起因 最近项目中有这样一个需求: 根据用户当前的坐标点,获取该用户附近的预设城市名称. 这里有一个注意点是,假设这些支持的城市名称是预设的,所以就不能直接通过地图类api根据坐标点获取所在城市名称了. 想到的解决思路是: 获取这几个预设城市的坐标点 App端获取用户当前坐标点 分别计算得到该用户坐标点距离各个预设城市的坐标点距离 然后计算得到其中距离最小的一项 这个坐标点对应的城市就是所求

  • Golang中map数据类型的使用方法

    目录 前言 案例 map map定义 map声明 map的操作 总结 前言 今天咱们来学习一下golang中的map数据类型,单纯的总结一下基本语法和使用场景,也不具体深入底层.map类型是什么呢?做过PHP的,对于数组这种数据类型是一点也不陌生了.PHP中的数组分为索引数组和关联数组.例如下面的代码: // 索引数组[数组的key是一个数字, 从0,1,2开始递增] $array = [1, '张三', 12]; // 关联数组[数组的key是一个字符串,可以自定义key的名称] $array

  • 浅析go中的map数据结构字典

    1. map的使用 golang中的map是一种数据类型,将键与值绑定到一起,底层是用哈希表实现的,可以快速的通过键找到对应的值. 类型表示:map[keyType][valueType] key一定要是可比较的类型(可以理解为支持==的操作),value可以是任意类型. 初始化:map只能使用make来初始化,声明的时候默认为一个为nil的map,此时进行取值,返回的是对应类型的零值(不存在也是返回零值).添加元素无任何意义,还会导致运行时错误.向未初始化的map赋值引起 panic: ass

  • golang中使用sync.Map的方法

    背景 go中map数据结构不是线程安全的,即多个goroutine同时操作一个map,则会报错,因此go1.9之后诞生了sync.Map sync.Map思路来自java的ConcurrentHashMap 接口 sync.map就是1.9版本带的线程安全map,主要有如下几种方法: Load(key interface{}) (value interface{}, ok bool) //通过提供一个键key,查找对应的值value,如果不存在,则返回nil.ok的结果表示是否在map中找到值

  • 深度解密 Go 语言中的 sync.map

    工作中,经常会碰到并发读写 map 而造成 panic 的情况,为什么在并发读写的时候,会 panic 呢?因为在并发读写的情况下,map 里的数据会被写乱,之后就是 Garbage in, garbage out,还不如直接 panic 了. 是什么 Go 语言原生 map 并不是线程安全的,对它进行并发读写操作的时候,需要加锁.而 sync.map 则是一种并发安全的 map,在 Go 1.9 引入. sync.map 是线程安全的,读取,插入,删除也都保持着常数级的时间复杂度. sync.

随机推荐