在Go中,在许多情况下,需要sync.Mutext
或chan
以防止同时访问共享对象。但是,在某些情况下,我只是对变量或对象字段的最新值感兴趣。还是我喜欢写一个值,不在乎另一个go例程以后是否覆盖它,还是以前才覆盖它。
这里是示例程序的两个变体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,不产生段错误以及不从内存中读取垃圾?
请解释为什么答案中的内容[。
Update
:我重写了该示例以更好地反映原始代码,其中我遇到了并发写入问题。重要的观点已经在评论中。我将接受一个答案,该答案以足够详细的方式总结了这些学习内容(尤其是在Go运行时中)。哪些Go数据类型可以安全地读取和安全地并发写入而无需使用mutext,不产生段错误以及不从内存中读取垃圾?
无。
真的很简单:无论如何在任何情况下都不能同时对Go中的任何内容进行读写。
(顺便说一句,您的“正确”程序不正确,它很讲究,即使您摆脱了竞争条件,它也不会确定地产生输出。)
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup // wait group to close channel
var buffer int = 1 // buffer of the channel
// channel to get the share data
cName := make(chan string, buffer)
for i := 0; i < 10; i++ {
wg.Add(1) // add to wait group
go func(i int) {
cName <- fmt.Sprintf("name = %d", i)
wg.Done() // decrease wait group.
}(i)
}
go func() {
wg.Wait() // wait of wait group to be 0
close(cName) // close the channel
}()
// process all the data
for n := range cName {
println("read:", n)
}
}
上面的代码返回以下输出
read: name = 0 read: name = 5 read: name = 1 read: name = 2 read: name = 3 read: name = 4 read: name = 7 read: name = 6 read: name = 8 read: name = 9
https://play.golang.org/p/R4n9ssPMOeS
但是,在某些情况下,我只是对变量或对象字段的最新值感兴趣。
这里是基本问题:“最新”一词是什么意思?
假设,从数学上讲,我们有一个值序列[[X
i],其中0 <= i
时间本身就失去了意义。我们不能说i
((在某些现代硬件中,实际实现由加载和存储缓冲区组成,可以重新排列加载和存储进入内存的顺序。barrier指令要么同步缓冲区,要么在缓冲区中放置实际的屏障,以便这种特殊的具体实现方式为思考问题提供了一种简便的方法,但是它并不完整:您应该将时间简单地理解为硬件提供的同步之外的不存在,即,all
从某个位置加载并存储到某个位置是同时发生的,而不是按顺序进行,除了这些障碍。)无论如何,Go的sync
软件包为您提供了一种访问这些类型障碍的简单高级访问方法。在互斥量Lock
调用之前执行的已编译代码确实确实完成了[[before锁定函数的返回,并且在调用之后执行的代码实际上直到after
锁定函数返回后才真正启动。Go的频道提供了相同的之前/之后时间保证。Go的sync/atomic
程序包提供了更低级别的保证。通常,应避免这样做,而应使用更高级别的通道或sync.Mutex
样式的保证。 (编辑以添加注释:您在这里可以
使用sync/atomic
的Pointer
操作,但不能直接与string
类型一起使用,因为Go字符串实际上是作为包含两个单独值的标头实现的:指针以及长度。您可以通过更新指向string
对象的指针,使用另一层间接方法解决此问题,但在考虑这样做之前,您应该对语言的首选方法进行基准测试并验证这些方法是一个问题,因为在sync/atomic
级别上工作的代码很难编写和调试。)