【发布时间】:2020-05-20 13:14:48
【问题描述】:
在 Go 中,sync.Mutex 或 chan 用于防止共享对象的并发访问。但是,在某些情况下,我只对对象的变量或字段的“最新”值感兴趣。
或者我喜欢写一个值,而不关心其他 go-routine 稍后是否覆盖它或之前刚刚覆盖它。
更新: TLDR;只是不要这样做。这是不安全的。阅读答案、cmets 和链接文档!
2021 年更新:Go 内存模型是 going to be specified more thoroughly,还有 Russ Cox 的 three great articles,它将教你更多关于非同步内存访问的惊人影响。这些文章总结了下面的很多讨论和学习。
以下是示例程序的两个变体good 和bad,它们似乎都使用当前的 Go 运行时产生“正确”的输出:
package main
import (
"flag"
"fmt"
"math/rand"
"time"
)
var bogus = flag.Bool("bogus", false, "use bogus code")
func pause() {
time.Sleep(time.Duration(rand.Uint32()%100) * time.Millisecond)
}
func bad() {
stop := time.After(100 * time.Millisecond)
var name string
// start some producers doing concurrent writes (DANGER!)
for i := 0; i < 10; i++ {
go func(i int) {
pause()
name = fmt.Sprintf("name = %d", i)
}(i)
}
// start consumer that shows the current value every 10ms
go func() {
tick := time.Tick(10 * time.Millisecond)
for {
select {
case <-stop:
return
case <-tick:
fmt.Println("read:", name)
}
}
}()
<-stop
}
func good() {
stop := time.After(100 * time.Millisecond)
names := make(chan string, 10)
// start some producers concurrently writing to a channel (GOOD!)
for i := 0; i < 10; i++ {
go func(i int) {
pause()
names <- fmt.Sprintf("name = %d", i)
}(i)
}
// start consumer that shows the current value every 10ms
go func() {
tick := time.Tick(10 * time.Millisecond)
var name string
for {
select {
case name = <-names:
case <-stop:
return
case <-tick:
fmt.Println("read:", name)
}
}
}()
<-stop
}
func main() {
flag.Parse()
if *bogus {
bad()
} else {
good()
}
}
预期的输出如下:
...
read: name = 3
read: name = 3
read: name = 5
read: name = 4
...
read: 和 read: name=[0-9] 的任何组合都是此程序的正确输出。接收任何其他字符串作为输出将是一个错误。
当使用go run --race bogus.go 运行这个程序时,它是安全的。
但是,go run --race bogus.go -bogus 会警告并发读取和写入。
对于map 类型和附加到切片时,我总是需要互斥锁或类似的保护方法来避免段错误或意外行为。但是,对变量或字段值读取和写入字面量(原子值)似乎是安全的。
问题:我可以安全地同时读取和安全地写入哪些 Go 数据类型,而无需使用 mutext,不会产生段错误,也不会从内存中读取垃圾?
请在您的回答中解释为什么某些东西在 Go 中是安全或不安全的。
更新:我重写了示例以更好地反映原始代码,其中我遇到了并发写入问题。重要的倾向已经在 cmets 中。我会接受一个足够详细地总结这些学习的答案(尤其是在 Go 运行时)。
【问题讨论】:
-
“似乎是安全的”并不能证明它是安全的。对于 any 值的并发读取和写入,您始终需要同步。
-
memory model 或语言规范中的任何内容都不需要编译器为
name赋值。一种可能的输出是name:的重复行 -
数据竞赛总是数据竞赛,这不是 Go 特有的。阅读:software.intel.com/content/www/us/en/develop/blogs/…
-
@Juve 这个讨论没有结果。您的代码是错误程序的主要示例,因为它很活泼。请用“你不能混合不同步的读取和写入。从不。对于所有数据类型,所有语法,真的是字面意思 never。
-
@Juve:你没抓住重点。它确实不依赖于实现来不产生虚假结果,这是一场数据竞赛,时期。不同的实现可能会产生不同的结果,但这些结果始终是未定义的,无法使用。
标签: go concurrency mutex shared-memory atomic