【问题标题】:Is it safe to read a function pointer concurrently without a lock?在没有锁的情况下同时读取函数指针是否安全?
【发布时间】:2017-05-15 08:39:23
【问题描述】:

假设我有这个:

go func() {
    for range time.Tick(1 * time.Millisecond) {
        a, b = b, a
    }
}()

还有其他地方:

i := a // <-- Is this safe?

对于这个问题,i 相对于原始ab 的值是什么并不重要。唯一的问题是阅读a 是否安全。也就是说,a 是否有可能是 nil、部分分配、无效、未定义、...不是有效值?

I've tried to make it fail 但到目前为止它总是成功(在我的 Mac 上)。

我无法在The Go Memory Model 文档中找到除此引用之外的任何具体内容:

读取和写入大于单个机器字的值的行为类似于 以未指定的顺序进行多个机器字大小的操作。

这是否暗示单个机器字写入实际上是原子的?如果是这样,Go 中的函数指针写入是否是单个机器字操作?

更新:这里是a properly synchronized solution

【问题讨论】:

  • 我想是的。看看这个:(play.golang.org/p/b-fyvCiR7b) 指针的大小总是4字节。在 32 位处理器中,字长为 32 位(4 字节)。显然,在 64 位处理器中,您有 8 个字节的字。因此,基于此以及您从文档中发布的 sn-p,我会说它是安全的。
  • @AmirKeibi 需要注意的重要一点是,文档中的 保证 说的是单机字操作 是原子的。它只是说大于单个机器字的操作是无序的。实际上,无论读写是否是单个机器字操作,仍然无法保证(Go 也无法保证)该操作是原子的。它依赖于硬件,从 Go 的角度来看是未定义的,因此需要同步。
  • 我认为问题标题有点误导。并发读取是安全的,但是当涉及到至少一个写入操作时,一切都会崩溃。

标签: go concurrency goroutine


【解决方案1】:

The Go Memory ModelThe Go Memory Model 对来自多个 goroutine 的任何变量的非同步、并发访问,其中至少一个是写入是 未定义的行为

未定义的意思是:未定义。您的程序可能会正常运行,也可能会无法正常运行。它可能会导致 Go 运行时提供的内存和类型安全丢失(参见下面的示例)。它甚至可能使您的程序崩溃。或者它甚至可能导致地球爆炸(这种可能性非常小,甚至可能小于 1e-40,但仍然......)。

这个未定义在您的情况下意味着是的,i 可能是nil,部分分配,无效,未定义,......除了ab 之外的任何东西。此列表只是所有可能结果的一小部分。

不要再认为某些数据竞争是(或可能是)良性或无害的。如果无人看管,它们可能是最糟糕的事情的根源。

由于您的代码在一个 goroutine 中写入变量 a 并在另一个 goroutine 中读取它(它试图将其值分配给另一个变量 i),因此这是一场数据竞争,因此不安全。在您的测试中它是否“正确”工作并不重要。可以将您的代码作为起点,对其进行扩展/构建,并由于您最初“无害”的数据竞争而导致灾难。

作为相关问题,请阅读How safe are Golang maps for concurrent Read/Write operations?Incorrect synchronization in go lang

强烈推荐阅读 Dmitry Vyukov 的博文:Benign data races: what could possibly go wrong?

还有一篇非常有趣的博客文章,其中展示了一个通过故意数据竞争破坏 Go 的内存安全的示例:Golang data races to break memory safety

【讨论】:

  • 像往常一样,非常好。我还建议 OP 阅读 this piece 以更好地掌握潜在的硬件问题。总结一下 OP,当他们在文本编辑器中编写代码时,代码“下方”存在两个问题:1)编译器可以生成机器代码,这些代码对变量的内存位置执行“奇怪”的事情; 2) 多 CPU 和/或多核硬件存在缓存一致性问题:当 CPU 从内存中读取值时,不一定是从另一个 CPU 写入的同一位置读取。
  • 哦,还有第三个问题:3) 广泛使用的硬件平台上的现代 CPU 经常执行内存访问的重新排序。所有这三个问题只有在程序员除了语言的内存模型保证之外还有期望时才会“破坏”,所以请不要有它们。 ;-)
  • @nicerobot 这就是我的观点。你没有保证i的值是有效的,这就是undefined的意思。你的程序甚至可能crash,我猜你不认为这是良性的。 可能使用当前的编译器、硬件和生成的代码,您不会遇到这种情况,但您无法保证。甚至可能下一个版本的 Go 编译器会生成一个不同的“优化”代码,它会根据您希望程序执行的操作“行为不端”——只是因为您在代码中留下了数据竞争。
  • @nicerobot,考虑使用sync/atomic 操作来访问您的值:您不会得到任何排序保证(并且您不需要它们),但在内存模型方面您将是安全的因为这些功能可确保在需要时进行适当的内存隔离。您可以在golang-nuts 邮件列表中搜索一个最近的线程来处理这个问题。
  • @nicerobot,仅供参考,这是我提到的the thread,特别是this response,来自与Load*()Store*()sync/atomic 打交道的核心团队成员之一,并且发生了- 在保证他们根据内存模型提供之前;它还提到了一个有趣的issue。希望这会引起您的兴趣。
【解决方案2】:

Race condition而言,它并不安全。简而言之,我对竞争条件的理解是,当有多个异步例程(协程、线程、进程、goroutine 等)试图访问同一资源并且至少一个是写入操作时,因此在您的示例中,我们有 2 个goroutines 读取和写入函数类型的变量,我认为从并发的角度来看,重要的是这些变量在某处有一个内存空间,我们正试图在这部分内存中读取或写入。

简答:只需使用-race 标志和go run -race 运行您的示例 或go build -race,您将看到检测到的数据竞争

【讨论】:

  • 这很好。但我不认为他的问题是关于比赛条件的。 “也就是说,i 是否有可能为 nil、部分赋值、无效、未定义……除了 a 或 b 之外的任何内容?”
  • 唯一的问题是分配给 i 是否安全
  • 不是真的,他澄清了问题是什么:“除了 a 或 b 之外的任何东西?”
  • @AmirKeibi 既然存在竞争条件,答案是肯定的。
  • 我的问题并不是关于数据竞赛。虽然-race 确实显示了一场比赛,但如果该值始终保证有效,我不在乎。我相信这最终更多是关于硬件问题,以及写入地址的值是否有可能在并发读取时处于无效状态。
猜你喜欢
  • 2010-11-23
  • 1970-01-01
  • 1970-01-01
  • 2023-04-09
  • 2019-09-05
  • 2020-06-16
  • 1970-01-01
  • 1970-01-01
  • 2021-10-03
相关资源
最近更新 更多