前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >Go错误集锦 | map中因mutex使用不当导致的数据竞争

Go错误集锦 | map中因mutex使用不当导致的数据竞争

作者头像
Go学堂
发布于 2023-01-31 08:11:48
发布于 2023-01-31 08:11:48
66700
代码可运行
举报
文章被收录于专栏:Go工具箱Go工具箱
运行总次数:0
代码可运行

大家好,我是「Go学堂」的渔夫子。今天跟大家分享一个使用mutex在对slice或map的数据进行保护时容易被忽略的一个案例。

众所周知,在并发程序中,对共享数据的访问是经常的事情,一般通过使用mutex对共享数据进行安全保护。当对slice和map使用mutex进行保护时有一个错误是经常被忽略的。下面我们看一个具体的示例。

我们首先定义一个Cache结构体,该结构体用来缓存客户的银行卡的当前余额数据。该结构体使用一个map来存储,key是客户的ID,value是客户的余额。同时,有一个保护并发访问的读写锁变量。如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type Cache struct {
    mu sync.RWMutex
    balances map[string]float64
}

接下来我们定义个AddBalance方法,该方法使用写锁来保护balances能被并发访问。如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func (c *Cache) AddBalance(id string, balance float64) {
    c.mu.Lock()
    c.balances[id] = balance
    c.mu.Unlock()
}

同时,我们还实现了一个求所有客户平均余额的函数。下面是其中的一种实现:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func (c *Cache) AverageBalance() float64 {
    c.mu.RLock()
    balances := c.balances
    c.mu.RUnlock()
    
    sum := 0.
    for _, balance := range balances {
        sum += balance
    }    
    return sum / float64(len(balances))
}

在该实现中,我们将c.balances拷贝到了一个本地变量中,然后就释放了锁。然后通过循环本地变量balances来计算所有客户的总额。最后返回客户的平均余额。以下是main中的代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func main() {
    cache := &Cache{
        balances : make(map[string]float64),
    }  

    go cache.AverageBalance()  
    go cache.AddBalance("ID-10", 100)
}

那么,这种实现方式有什么问题吗?如果我们使用-race运行,则会提示导致数据竞争。所以这里的问题处在哪里呢?

实际上,我们在之前讲过map的底层数据结构实际上是一些元信息加上一个指向buckets的数据指针。因此,当使用balances := c.balances时并没有拷贝实际的数据。而只是拷贝了map的元信息而已。如下图:

这里只列出了map底层结构体的关键字段,若想了解map底层的详细结构可以参考我之前的那篇 map的底层实现原理。由上图可以看到两个变量底层指向的数组实际上是同一个内存地址。在并发中,两个协程同时操作一个内存地址的数据,而且其中一个是写入操作,因此就造成了数据竞争。

那我们应该如何避免该数据竞争呢?我们有两种方式。

一种方式是当迭代的逻辑如果耗时不是很大的话,可以扩大临界区。如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func (c *Cache) AverageBalance() float64 {
    c.mu.RLock()    defer c.mu.RUnlock()

    sum := 0
    for _, balance := range c.balances {
        sum += balance
    }    
    return sum / float64(len(c.balances))
}

在该实现中,整个函数都是临界区,这样也就避免了数据竞争。

第二种方式是将原来的map数据深度拷贝一份到本地变量。这种方式适用于迭代循环逻辑比较重(也就是耗时比较大)的场景。比如在迭代逻辑中会涉及到网络IO(数据库的读写等)。如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func (c *Cache) AverageBalance() float64 {
    c.mu.RLock()
    m := make(map[string]float64, len(c.balances))
    for k, v := range c.balances {
        m[k] = v
    }
    c.mu.RUnlock()

    sum := 0
    for _, balance := range balances {
        sum += balance
    }    
    return sum / float64(len(c.balances))
}

在这种实现方案中,一旦我们完成了深度拷贝,就将锁给释放。同时,迭代的逻辑在临界区外实现。

总之,当我们使用互斥锁时一定要格外注意临界区。今天的分享就到这里了。


欢迎关注「Go学堂」,让知识活起来

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-06-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Go学堂 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Go语言中常见100问题-#70 Using mutexes inaccurately with slices and maps
在并发环境下,当要处理的数据存在变化并且是共享的时候,我们一般使用互斥锁(mutex)来保护数据对象读写操作。一个常见的错误是在使用切片和map时没有准确地使用互斥锁操作。下面通过一个具体的例子来说明并分析它存在的问题。
数据小冰
2022/08/15
1950
goroutine 并发中竞争条件的解决
上一篇文章,我们详细介绍了通过 goroutine 和通道来实现并发编程: GoLang 的并发编程与通信 — goroutine 与通道
用户3147702
2022/06/27
1.2K0
goroutine 并发中竞争条件的解决
Cluster版本中的Meta
Cluster版本中的Meta Metadata Client Metadata Client概述 定义在 services/meta/client.go中; Cluster 版本中的Meta是本地的一个内存缓存,数据来源MetaServer; 对Meta的所有写操作,也将通过http+pb的方式发送到MetaServer, 然后阻塞等待从MetaServer返回的新的Metadata通知; MetaClient通过http long polling来及时获取Metadata的变化; 所有和Meta dat
扫帚的影子
2018/12/12
6260
Go语言核心36讲(Go语言实战与应用四)--学习笔记
从本篇文章开始,我们将一起探讨 Go 语言自带标准库中一些比较核心的代码包。这会涉及这些代码包的标准用法、使用禁忌、背后原理以及周边的知识。
郑子铭
2021/11/14
3120
Go语言核心36讲(Go语言实战与应用四)--学习笔记
Go语言基于共享变量的并发
一个特定类型的方法和操作函数是并发安全的,那么所有它的访问方法和操作都是并发安全的。导出包级别的函数一般情况下都是并发安全的,package级的变量没法被限制在单一的goroutine,所以修改这些变量必须使用互斥条件。 竞争条件指的是程序在多个goroutine交叉执行操作时,没有给出正确的结果。只要有两个goroutine并发访问同一个变量,且至少其中的一个是写操作的时候就会发生数据竞争。 避免数据竞争的方法: >> 方法不要去写变量,此时指只在第一次创建时写入,后续不再对该变量进行修改。
李海彬
2018/03/22
1.4K0
Go 并发编程之 Mutex
友情提示:此篇文章大约需要阅读 18分钟0秒,不足之处请多指教,感谢你的阅读。 订阅本站
Meng小羽
2020/11/23
6220
Go 并发编程之 Mutex
Go语言中常见100问题-#72 Forgetting about sync.Cond
Go标准库中的sync包提供了常用的同步原语功能,该包中有一个结构我们可能很少使用也容易忽视,它就是sync.Cond,但是它有一个特色功能,能够实现通道(channel)不能实现的功能,所以我们不要忽视它的存在。本文将通过一个具体的例子来了解sync.Cond用在什么场合下以及如何使用它。
数据小冰
2022/08/15
1.2K0
Go语言中常见100问题-#72 Forgetting about sync.Cond
《Go语言程序设计》读书笔记(七)基于共享变量的并发
上面的例子里Unlock会在return语句读取完balance的值之后执行,所以Balance函数是并发安全的。
KevinYan
2020/01/13
3770
GO 语言处理并发的时候我们是选择sync还是channel
以前写 C 的时候,我们一般是都通过共享内存来通信,对于并发去操作某一块数据时,为了保证数据安全,控制线程间同步,我们们会去使用互斥锁,加锁解锁来进行处理
阿兵云原生
2023/10/24
2340
GO 语言处理并发的时候我们是选择sync还是channel
Golang缓存库 go-cache
go-cache 是一个类似Memcached的go库,key:value存储在内存中。适合单机应用调用。
后端云
2022/11/25
1.7K0
Go 语言并发编程系列(十)—— sync 包系列:互斥锁和读写锁
我们前面反复强调,在 Go 语言并发编程中,倡导「使用通信共享内存,不要使用共享内存通信」,而这个通信的媒介就是我们前面花大量篇幅介绍的通道(Channel),通道是线程安全的,不需要考虑数据冲突问题,面对并发问题,我们始终应该优先考虑使用通道,它是 first class 级别的,但是纵使有主角光环加持,通道也不是万能的,它也需要配角,这也是共享内存存在的价值,其他语言中主流的并发编程都是通过共享内存实现的,共享内存必然涉及并发过程中的共享数据冲突问题,而为了解决数据冲突问题,Go 语言沿袭了传统的并发编程解决方案 —— 锁机制,这些锁都位于 sync 包中。
学院君
2019/09/10
8880
Go基于共享变量的并发原理及实例 【Go语言圣经笔记】
前一章我们介绍了一些使用goroutine和channel这样直接而自然的方式来实现并发的方法。然而这样做我们实际上回避了在写并发代码时必须处理的一些重要而且细微的问题(笔者注:一谈到并发,就需要处理对共享变量等公共资源的访问问题,不合理的访问问题会造成一系列诸如丢失修改、读脏数据、重复读等常见并发问题)。
Steve Wang
2021/12/06
1K0
Go语言入门(八)线程安全&锁
线程安全&锁 定时器&一次性定时器 定时器 func main() { ticker := time.NewTicker(time.Second) //ticker.C是一个只读的chan,所以直接可以使用for range读取 for v := range ticker.C { fmt.Printf("hello %v\n",v) //按秒输出 } } 一次性定时器 func main() { select { case <- time.After(ti
alexhuiwang
2020/09/24
3850
Go错误集锦 | 字符串格式化竟然能引起死锁
今天跟大家分享一个关于格式化字符串时造成的死锁现象及对应的解决方案。以便大家在今后的研发中可以避免类似情况的出现。
Go学堂
2023/01/31
2700
<源码阅读>Go Cache
项目地址 https://github.com/patrickmn/go-cache
Porco1Rosso
2021/04/27
2.1K1
<源码阅读>Go Cache
100 个 Go 错误以及如何避免:9~12
在前一章中,我们讨论了并发的基础。现在是时候看看 Go 开发人员在使用并发原语时所犯的实际错误了。
ApacheCN_飞龙
2023/10/13
9110
100 个 Go 错误以及如何避免:9~12
Go 语言入门三部曲(一):能看懂 Go 语言
1、能看懂 Go 语言 <- 拿到钥匙了 2、能用 Go 语言写管理系统 <- 趴门墩儿上了 3、能用 Go 语言写 “生产/消费者” 模型 <- 进门了,广阔天地大有可为
看、未来
2022/06/15
4920
Go 语言入门三部曲(一):能看懂 Go 语言
Go Mutex:保护并发访问共享资源的利器
Go 语言以 高并发 著称,其并发操作是重要特性之一。虽然并发可以提高程序性能和效率,但同时也可能带来 竞态条件 和 死锁 等问题。为了避免这些问题,Go 提供了许多 并发原语,例如 Mutex、RWMutex、WaitGroup、Channel 等,用于实现同步、协调和通信等操作。
陈明勇
2023/04/24
5720
Go Mutex:保护并发访问共享资源的利器
Go常见错误集锦之range常踩的那些坑
在Go语言中,for-range是常用的循环控制语句。本文就带你一起来踩踩使用range时的那些坑。
Go学堂
2023/01/31
7350
如何设计并实现一个线程安全的 Map ?(下篇)
在上篇中,我们已经讨论过如何去实现一个 Map 了,并且也讨论了诸多优化点。在下篇中,我们将继续讨论如何实现一个线程安全的 Map。说到线程安全,需要从概念开始说起。
一缕殇流化隐半边冰霜
2018/08/30
2.2K0
如何设计并实现一个线程安全的 Map ?(下篇)
推荐阅读
相关推荐
Go语言中常见100问题-#70 Using mutexes inaccurately with slices and maps
更多 >
领券
社区富文本编辑器全新改版!诚邀体验~
全新交互,全新视觉,新增快捷键、悬浮工具栏、高亮块等功能并同时优化现有功能,全面提升创作效率和体验
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文