前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go语言中常见100问题-#74 Copying a sync type

Go语言中常见100问题-#74 Copying a sync type

作者头像
数据小冰
发布2022-08-15 15:27:57
2950
发布2022-08-15 15:27:57
举报
文章被收录于专栏:数据小冰
复制sync包中类型的变量时需小心谨慎

sync包提供了基本的同步原语,像互斥锁(sync.Mutex)、条件变量(sync.Cond)、等待组(sync.WaitGroup)等。对于所有这些类型,有一条硬性规则需要我们遵守:不能对这些类型的变量进行复制使用。本文讨论它们的工作原理以及如果进行复制使用会导致什么问题。

下面程序实现了一个计数存储功能,并且是线程安全的。Counter结构中的map[string]int表示每个计数器的当前值,为了保证其并发访问操作的安全性,使用sync.Mutex保护它,Add方法实现计数增加功能。代码如下, 对counters计数放在临界区中,即放在c.mu.Lock()c.mu.Unlock()中。

代码语言:javascript
复制
type Counter struct {
        mu       sync.Mutex
        counters map[string]int
}

func NewCounter() Counter {
        return Counter{counters: map[string]int{}}
}

func (c Counter) Increment(name string) {
        c.mu.Lock()
        defer c.mu.Unlock()
        c.counters[name]++
}

下面启动两个goroutine对counter进行自增操作,在运行时加入-race参数进行数据竞争检查,看看会产生什么情况。

代码语言:javascript
复制
counter := NewCounter()

go func() {
        counter.Increment("foo")
}()
go func() {
        counter.Increment("bar")
}()

完整代码见https://github.com/ThomasMing0915/100-go-mistakes-code/tree/main/74,程序输出结果如下。什么?,竟然存在数据竞争?

代码语言:javascript
复制
go run -race example1.go                                               
==================
WARNING: DATA RACE
Read at 0x00c0000a0060 by goroutine 7:
  runtime.mapaccess1_faststr()

上述代码存在数据竞争的原因是互斥锁(mu)被复制了,因为Increment操作的接收者是值类型,所以当我们每次调用Increment时,它都会对当前的Counter进行复制,Counter内部的互斥锁也被复制了。sync包中的类型不能被复制使用,像下面列举的类型都是不能进行复制使用的.

  • sync.Cond
  • sync.Map
  • sync.Mutex
  • sync.RWMutex
  • sync.Once
  • sync.Pool
  • sync.WaitGroup

既然知道了上述程序问题所在,现在的问题是如何解决呢?主要有两种解决方法。

第一种解决方法是,将Increment方法的接收者从值类型改为指针类型,代码如下。通过修改接收者类型,可以避免调用Increment时复制Counter,进而避免内部互斥锁复制。

代码语言:javascript
复制
func (c *Counter) Increment(name string) {
        // Same code
}

第二种解法方法是,如果不想调整接收者的类型,可以将Counter结构体中mu字段的类型改为指针类型,代码如下。虽然Increment的接收者类型还是值类型,调用时会复制Counter结构,但是由于mu是一个指针,复制后指针指向的对象和被复制对象指针指向都是同一个对象,所以不存在数据竞争问题。

代码语言:javascript
复制
type Counter struct {
        mu       *sync.Mutex
        counters map[string]int
}

func NewCounter() Counter {
        return Counter{
                mu: &sync.Mutex{},
                counters: map[string]int{},
        }
}

「NOTE: 在第二种解决方法中,我们将mu字段定义为指针类型,这个时候在创建Counter时需要进行初始化。如果省略它,mu的值会被初始化为指针的零值(nil),在调用c.mu.Lock()会产生panic.」

在下面的情况中,我们可能会遇到无意复制sync字段的问题,在编写程序时应该小心谨慎。

  • 调用值接收器的方法(像本文中的例子),值对象结构体定义中含有sync包中类型
  • 将sync包中的类型变量作为函数入参传递
  • 函数入参变量类型结构体定义中含有sync包中类型

此外,使用一些静态代码检查工具linter可以扫描出这类问题。例如,使用go vet 检查前面的程序,输出结果如下:

代码语言:javascript
复制
go vet .
./example1.go:19:9: Increment passes lock by value: Counter contains sync.Mutex

总结:当多个goroutine需要访问一个公共的sync包中对象时,我们必须确保它们都依赖于同一个实例。该规则适用于sync包定义的所有类型,使用指针而不是值是解决这种问题的一个方法:将结构体中用到的sync包中类型的字段定义为指针类型,或者使用结构体的指针对象。

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

本文分享自 数据小冰 微信公众号,前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 复制sync包中类型的变量时需小心谨慎
相关产品与服务
腾讯云代码分析
腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,支撑团队传承代码文化。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档