【问题标题】:What happens when reading or writing concurrently without a mutex在没有互斥体的情况下同时读取或写入时会发生什么
【发布时间】:2020-05-20 13:14:48
【问题描述】:

在 Go 中,sync.Mutexchan 用于防止共享对象的并发访问。但是,在某些情况下,我只对对象的变量或字段的“最新”值感兴趣。 或者我喜欢写一个值,而不关心其他 go-routine 稍后是否覆盖它或之前刚刚覆盖它。

更新: TLDR;只是不要这样做。这是不安全的。阅读答案、cmets 和链接文档!

2021 年更新:Go 内存模型是 going to be specified more thoroughly,还有 Russ Cox 的 three great articles,它将教你更多关于非同步内存访问的惊人影响。这些文章总结了下面的很多讨论和学习。

以下是示例程序的两个变体goodbad,它们似乎都使用当前的 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


【解决方案1】:

但是,在某些情况下,我只对对象的变量或字段的最新值感兴趣。

这是一个根本问题:“最新”这个词是什么意思?

假设,从数学上讲,我们有一个值序列 Xi,其中 0 。那么显然 Xj 是“晚于” Xi if j > i .这是“最新”的一个很好的简单定义,并且可能是您想要的。

但是,当一台机器中的两个独立 CPU(包括 Go 程序中的两个 goroutine)同时工作时,时间本身就失去了意义。我们不能说是 i j。所以latest这个词没有正确的定义。

为了解决这类问题,现代 CPU 硬件和 Go 作为编程语言为我们提供了某些同步原语。如果 CPU A 和 B 执行内存栅栏指令或同步指令,或使用任何其他硬件规定,CPU(和/或一些外部硬件)将插入“时间”概念所需的任何内容以恢复其含义。也就是说,如果 CPU 使用屏障指令,我们可以说在屏障之前执行的内存加载或存储是“之前”,而在之后执行的内存加载或存储 障碍是“之后”。

(在某些现代硬件中,实际实现由加载和存储缓冲区组成,它们可以重新排列加载和存储进入内存的顺序。屏障指令要么同步缓冲区,要么在其中放置一个实际的屏障,所以加载和存储不能越过障碍。这个特定的具体实现提供了一种简单的方法来思考问题,但并不完整:您应该将时间简单地视为不存在硬件之外-提供同步,即所有从某个位置加载和存储到某个位置是同时发生的,而不是按某种顺序发生,除了这些障碍。)

无论如何,Go 的 sync 包为您提供了一种简单的高级访问方法来访问这些障碍。在互斥体Lock 调用之前执行的编译代码确实完成了锁函数返回之前,并且在调用之后执行的代码真正直到锁之后才开始函数返回。

Go 的频道提供相同类型的前后时间保证。

Go 的 sync/atomic 包提供了更低级别的保证。一般来说,您应该避免这种情况,而支持更高级别的频道或sync.Mutex 样式保证。 (编辑添加注释:您可以在此处使用sync/atomicPointer 操作,但不能直接使用string 类型,因为Go 字符串实际上是作为包含两个单独值的标头实现的: 一个指针和一个长度。您可以通过更新指向string 对象的指针,通过另一层间接来解决这个问题。但在您考虑这样做之前,您应该对语言的首选方法的使用进行基准测试和验证这些是否存在问题,因为在 sync/atomic 级别工作的代码很难编写和调试。)

【讨论】:

  • “最新”是指可以从共享内存读取的最后写入值。在消费者读取值之前最后写入内存的生产者 go-routine 将赢得比赛。但是要使其正常工作,读取和写入需要是原子的,即,您不能只向/从内存写入/读取“半个值”。但正如你上面所说,这实际上可能发生在 Go 中,例如,使用字符串,这应该会导致不需要的字符串值。您是否有一些链接,我们可以在其中找到有关 Go 运行时中这些非原子内存读取和写入的更多详细信息/示例/问题?
  • 没有“最后”,不在硬件层面。时间不再意味着任何东西,并非没有障碍指令。由于 write-behind write-buffering,两个 CPU 对于“最后写入”的值可能存在分歧!见software.intel.com/content/dam/develop/public/us/en/documents/…
  • 当你使用sync/atomic时,它会调用特殊的编译器指令来使用特殊的屏障指令并确保写入缓冲区被刷新等,那么有一个“最后的”。但是,当您不这样做时,就没有。 mutex 包建立在sync/atomic 之上,并隐藏了剩余的特殊情况。
  • 呃,看起来英特尔再次移动了文档(它在另一个链接中建议了我这次发布的链接)。我必须找到他们把它放在哪里。
  • 啊,不,那个链接没问题。请参阅第 8.2 节(尤其是 8.2.2 和后续部分)关于 x86 多 CPU 系统中的时间性;有非临时存储指令,编译器可以选择在各种情况下使用。请注意,x86 硬件也是更宽容的硬件之一,因为底层硬件提供了非常强大的内存排序模型。 Go 构建的其他 CPU 使用较弱的排序。
【解决方案2】:

我可以安全地同时读取和安全地写入哪些 Go 数据类型,而无需使用 mutext,不会产生段错误,也不会从内存中读取垃圾?

无。

真的就这么简单:在任何情况下,您都不能同时读取和写入 Go 中的任何内容。

(顺便说一句:您的“正确”程序不正确,它很活泼,即使您摆脱了竞争条件,它也不会确定性地产生输出。)

【讨论】:

    【解决方案3】:

    为什么不能使用频道

    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

    Article about channels

    【讨论】:

    • 是的,频道将是最好的解决方案:我也只是重写了它:play.golang.org/p/RByVuPksc8c with channels。
    • 而不是使用time.Sleep(),使用sync.WaitGroups。
    猜你喜欢
    • 2011-07-23
    • 1970-01-01
    • 2017-02-20
    • 2019-01-17
    • 2018-07-26
    • 2020-04-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多