脚本之家,脚本语言编程技术及教程分享平台!
分类导航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|

服务器之家 - 脚本之家 - Golang - 使用Go语言时,谨防锁拷贝!

使用Go语言时,谨防锁拷贝!

2021-07-29 00:07Golang来啦Seekload Golang

相信大家对 Go 语言的锁拷贝问题并不陌生,那我们应该如何规范使用Go 语言才能规避这个问题呢?一起来看作者是如何处理的。

使用Go语言时,谨防锁拷贝!

四哥水平有限,如有翻译或理解错误,烦请帮忙指出,感谢!

相信大家对 Go 语言的锁拷贝问题并不陌生,那我们应该如何规范使用Go 语言才能规避这个问题呢?一起来看作者是如何处理的。

原文如下:

假设我们有一个包含 map 的结构体,现在想在方法中修改这个 map,看下面的例子[1]:

  1. package main 
  2.  
  3. import "fmt" 
  4.  
  5. type Container struct { 
  6.   counters map[string]int 
  7.  
  8. func (c Container) inc(name string) { 
  9.   c.counters[name]++ 
  10.  
  11. func main() { 
  12.   c := Container{counters: map[string]int{"a": 0, "b": 0}} 
  13.  
  14.   doIncrement := func(name string, n int) { 
  15.     for i := 0; i < n; i++ { 
  16.       c.inc(name
  17.     } 
  18.   } 
  19.  
  20.   doIncrement("a", 100000) 
  21.  
  22.   fmt.Println(c.counters) 

Container 包含一个计数器集合,按 name 区分。inc() 会按 name 对相应的计数器执行自增操作(假设计数器存在)。main() 里循环多次调用 inc()。

执行上面的代码,输出:

  1. map[a:100000 b:0] 

现在假设有两个 goroutine 会并发地调用 inc()。因为我们必须小心竞争条件,所以使用了 Mutex 保护临界区。

  1. package main 
  2.  
  3. import ( 
  4.   "fmt" 
  5.   "sync" 
  6.   "time" 
  7.  
  8. type Container struct { 
  9.   sync.Mutex                       // <-- Added a mutex 
  10.   counters map[string]int 
  11.  
  12. func (c Container) inc(name string) { 
  13.   c.Lock()                         // <-- Added locking of the mutex 
  14.   defer c.Unlock() 
  15.   c.counters[name]++ 
  16.  
  17. func main() { 
  18.   c := Container{counters: map[string]int{"a": 0, "b": 0}} 
  19.  
  20.   doIncrement := func(name string, n int) { 
  21.     for i := 0; i < n; i++ { 
  22.       c.inc(name
  23.     } 
  24.   } 
  25.  
  26.   go doIncrement("a", 100000) 
  27.   go doIncrement("a", 100000) 
  28.  
  29.   // Wait a bit for the goroutines to finish 
  30.   time.Sleep(300 * time.Millisecond) 
  31.   fmt.Println(c.counters) 

你期望上面这段代码会输出什么呢?我得到的结果是这样的:

  1. func (c *Container) inc(name string) { 
  2.   c.Lock() 
  3.   defer c.Unlock() 
  4.   c.counters[name]++ 

我们使用 mutex 时已经很小心了,怎么还会出问题呢?你觉得应该如何修复这个问题?提示:只需要改动一个字符的代码就可以了!

代码的问题在于,无论何时调用 inc(),c 都会是一份拷贝,因为 inc() 是定义在 Container 上,而非 *Container;换句话说,c 是值接受者,而不是指针接受者。因此,inc() 并不能真正修改 c 的内容。

但等等,文章第一个示例是如何工作的?在单协程的例子中,c 也是按值传递,但是为什么能得到正确的结果 -- 在 inc() 在对 map 所做的修改,能影响到 main() 函数的原始值。这是因为 map 是引用类型而非值类型。Container 里保存的是指向 map 的指针,而不是 map 实际的数据。所以即使我们创建 Container 的副本,counters 保存的仍是指向 map 的地址。

所以文章第一个例子也是存在问题的,尽管执行结果没有问题,但是使用方法不符合官方指南[2] - 在方法中对原始数据进行修改,则方法应定义成指针方法,而非值方法。这里对 map 的使用给了我们一种错误的提示。作为练习,可以将第一个示例中的 map 换成 int 类型的计数器,并注意观察 inc() 的副本是如何递增的,在 inc() 中对副本做的修改不会影响到 main() 中的原始值。

Mutex 是值类型(可以看 Go 文档[3]相关的定义,包括注释里也明确地提示不能拷贝),复制再使用是错误的。复制仅仅是创建了一个新的 mutex,很显然地,对计数器的互斥使用就失效了。

所以应该这样修改,定义 inc() 方法时在 Container 之前添加 *:

  1. func (c *Container) inc(name string) { 
  2.   c.Lock() 
  3.   defer c.Unlock() 
  4.   c.counters[name]++ 

c 通过指针方式传到方法中,指向的 Container 与 main() 函数里面的是同一个。

这个问题并不罕见,事实上,使用 go vet 命令就会发现这个问题:

  1. $ go tool vet method-mutex-value-receiver.go 
  2. method-mutex-value-receiver.go:19: inc passes lock by value: main.Container 

在我看来,实际上这个问题帮助我们理清了值接收者与指针接收者之间的区别。为了说明这一点,下面还有一个示例,这个示例与上面两个示例没有关系。这个示例使用到了 & 取值符和 %p 格式化输出变量的地址。

  1. package main 
  2.  
  3. import "fmt" 
  4.  
  5. type Container struct { 
  6.   i int 
  7.   s string 
  8.  
  9. func (c Container) byValMethod() { 
  10.   fmt.Printf("byValMethod got &c=%p, &(c.s)=%p\n", &c, &(c.s)) 
  11.  
  12. func (c *Container) byPtrMethod() { 
  13.   fmt.Printf("byPtrMethod got &c=%p, &(c.s)=%p\n", c, &(c.s)) 
  14.  
  15. func main() { 
  16.   var c Container 
  17.   fmt.Printf("in main &c=%p, &(c.s)=%p\n", &c, &(c.s)) 
  18.  
  19.   c.byValMethod() 
  20.   c.byPtrMethod() 

执行代码后输出(如果在你的机器上执行,输出的地址可能不同,但是这不影响说明问题):

  1. in main &c=0xc00000a060, &(c.s)=0xc00000a068 
  2. byValMethod got &c=0xc00000a080, &(c.s)=0xc00000a088 
  3. byPtrMethod got &c=0xc00000a060, &(c.s)=0xc00000a068 

main() 函数里创建了 Container 变量 c,并且输出它的地址和它的成员 s 的地址,接着调用了 Container 的两个方法。byValMethod() 是值接受者,因为是原值的拷贝所有打印的地址不一样。另一方面,byPtrMethod() 是指针接收者,输出的地址与 main() 函数输出的地址一致,因为调用时获取的是 c 实际的地址,而不是副本。

参考资料

[1]例子: https://github.com/eliben/code-for-blog/tree/master/2018/go-copying-mutex

[2]官方指南: https://golang.org/doc/faq#methods_on_values_or_pointers

[3]Go 文档: https://golang.org/src/sync/mutex.go

原文链接:https://mp.weixin.qq.com/s/zLbd0PclO9g6W0dg7pT3AQ

延伸 · 阅读

精彩推荐
  • GolangGo语言range关键字循环时的坑

    Go语言range关键字循环时的坑

    今天小编就为大家分享一篇关于Go语言range关键字循环时的坑,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来...

    benben_20154202020-05-23
  • GolangGolang 语言极简类型转换库cast的使用详解

    Golang 语言极简类型转换库cast的使用详解

    本文我们通过 cast.ToString() 函数的使用,简单介绍了cast 的使用方法,除此之外,它还支持很多其他类型,在这没有多多介绍,对Golang 类型转换库 cast相关知...

    Golang语言开发栈6112021-12-02
  • Golanggo语言获取系统盘符的方法

    go语言获取系统盘符的方法

    这篇文章主要介绍了go语言获取系统盘符的方法,涉及Go语言调用winapi获取系统硬件信息的技巧,具有一定参考借鉴价值,需要的朋友可以参考下 ...

    无尽海3862020-04-24
  • GolangGolang实现四种负载均衡的算法(随机,轮询等)

    Golang实现四种负载均衡的算法(随机,轮询等)

    本文介绍了示例介绍了Golang 负载均衡的四种实现,主要包括了随机,轮询,加权轮询负载,一致性hash,感兴趣的小伙伴们可以参考一下...

    Gundy_8442021-08-09
  • GolangGo语言基础单元测试与性能测试示例详解

    Go语言基础单元测试与性能测试示例详解

    这篇文章主要为大家介绍了Go语言基础单元测试与性能测试示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助祝大家多多进步...

    枫少文7812021-12-05
  • GolangGo语言实现自动填写古诗词实例代码

    Go语言实现自动填写古诗词实例代码

    这篇文章主要给大家介绍了关于Go语言实现自动填写古诗词的相关资料,这是最近在项目中遇到的一个需求,文中通过示例代码介绍的非常详细,需要的朋...

    FengY5862020-05-14
  • GolangGO语言字符串处理Strings包的函数使用示例讲解

    GO语言字符串处理Strings包的函数使用示例讲解

    这篇文章主要为大家介绍了GO语言字符串处理Strings包的函数使用示例讲解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步早日升职加...

    Jeff的技术栈6882022-04-14
  • Golang深入浅析Go中三个点(...)用法

    深入浅析Go中三个点(...)用法

    这篇文章主要介绍了深入浅析Go中三个点(...)用法,需要的朋友可以参考下...

    踏雪无痕SS6472021-11-17