英文:
Is it safe to read a function pointer concurrently without a lock?
问题
假设我有以下代码:
go func() {
for range time.Tick(1 * time.Millisecond) {
a, b = b, a
}
}()
在其他地方:
i := a // <- 这样安全吗?
对于这个问题,关于i
相对于原始的a
或b
的值是什么并不重要。唯一的问题是读取a
是否安全。也就是说,a
可能是nil
、部分赋值、无效、未定义,或者除了有效值之外的任何其他值吗?
我尝试让它失败,但到目前为止它总是成功的(在我的 Mac 上)。
我在《Go 内存模型》文档中除了以下引用之外,没有找到其他具体的信息:
大于一个机器字的值的读取和写入行为类似于多个机器字大小的操作,顺序不确定。
这是否意味着单个机器字的写入是原子的?如果是这样,那么在 Go 中函数指针的写入是否是单个机器字操作?
更新: 这里有一个正确同步的解决方案。
英文:
Suppose I have this:
go func() {
for range time.Tick(1 * time.Millisecond) {
a, b = b, a
}
}()
And elsewhere:
i := a // <-- Is this safe?
For this question, it's unimportant what the value of i
is with respect to the original a
or b
. The only question is whether reading a
is safe. That is, is it possible for a
to be nil
, partially assigned, invalid, undefined, ... anything other than a valid value?
I've tried to make it fail but so far it always succeeds (on my Mac).
I haven't been able to find anything specific beyond this quote in the The Go Memory Model doc:
> Reads and writes of values larger than a single machine word behave as
> multiple machine-word-sized operations in an unspecified order.
Is this implying that a single machine word write is effectively atomic? And, if so, are function pointer writes in Go a single machine word operation?
Update: Here's a properly synchronized solution
答案1
得分: 25
在至少有一个写操作的情况下,从多个goroutine并发访问任何变量而没有同步的行为是未定义行为,这是由Go内存模型定义的。
未定义意味着它的含义就是它的字面意思:未定义。你的程序可能会正常工作,也可能会工作不正确。它可能导致丢失Go运行时提供的内存和类型安全性(参见下面的示例)。甚至可能导致程序崩溃。或者甚至可能导致地球爆炸(这种可能性非常小,甚至可能小于1e-40,但仍然...)。
在你的情况下,这个_未定义_意味着是的,i
可能是nil
,部分赋值,无效,未定义的,...除了a
或b
之外的任何其他值。这个列表只是所有可能结果的一个小小子集。
不要认为某些数据竞争是(或可能是)良性或无害的。如果不加以处理,它们可能成为最糟糕的事情的源头。
由于你的代码在一个goroutine中写入变量a
,并在另一个goroutine中读取它(尝试将其值赋给另一个变量i
),这是一个数据竞争,因此不安全。无论在你的测试中它是否工作“正确”,都没有关系。有人可能以你的代码为起点,进行扩展/构建,并由于最初“无害”的数据竞争而导致灾难。
作为相关问题,阅读https://stackoverflow.com/questions/36167200/how-safe-are-golang-maps-for-concurrent-read-write-operations/36167693#36167693和https://stackoverflow.com/questions/33274920/incorrect-synchronization-in-go-lang/33275092#33275092。
强烈推荐阅读Dmitry Vyukov的博文:良性数据竞争:可能出现什么问题?
还有一篇非常有趣的博文,展示了一个通过有意的数据竞争来破坏Go内存安全性的示例:Golang数据竞争破坏内存安全性
英文:
Unsynchronized, concurrent access to any variable from multiple goroutines where at least one of them is a write is undefined behavior by The Go Memory Model.
Undefined means what it says: undefined. It may be that your program will work correctly, it may be it will work incorrectly. It may result in losing memory and type safety provided by the Go runtime (see example below). It may even crash your program. Or it may even cause the Earth to explode (probability of that is extremely small, maybe even less than 1e-40, but still...).
This undefined in your case means that yes, i
may be nil
, partially assigned, invalid, undefined, ... anything other than either a
or b
. This list is just a tiny subset of all the possible outcomes.
Stop thinking that some data races are (or may be) benign or unharmful. They can be the source of the worst things if left unattended.
Since your code writes to the variable a
in one goroutine and reads it in another goroutine (which tries to assign its value to another variable i
), it's a data race and as such it's not safe. It doesn't matter if in your tests it works "correctly". One could take your code as a starting point, extend / build on it and result in a catastrophe due to your initially "unharmful" data race.
As related questions, read https://stackoverflow.com/questions/36167200/how-safe-are-golang-maps-for-concurrent-read-write-operations/36167693#36167693 and https://stackoverflow.com/questions/33274920/incorrect-synchronization-in-go-lang/33275092#33275092.
Strongly recommended to read the blog post by Dmitry Vyukov: Benign data races: what could possibly go wrong?
Also a very interesting blog post which shows an example which breaks Go's memory safety with intentional data race: Golang data races to break memory safety
答案2
得分: 5
从竞态条件的角度来看,它是不安全的。简而言之,我对竞态条件的理解是当有多个异步例程(协程、线程、进程、goroutine等)尝试访问同一资源,并且至少有一个是写操作时,就会出现竞态条件。所以在你的例子中,我们有两个goroutine读取和写入函数类型的变量,我认为从并发的角度来看,重要的是这些变量在某个内存空间中,并且我们试图在该内存区域中进行读取或写入。
简短回答:只需使用-race
标志运行你的示例,例如go run -race
或go build -race
,你将看到检测到的数据竞争。
英文:
In terms of Race condition, it's not safe. In short my understanding of race condition is when there're more than one asynchronous routine (coroutines, threads, process, goroutines etc.) trying to access the same resource and at least one is a writing operation, so in your example we have 2 goroutines reading and writing variables of type function, I think what's matter from a concurrent point of view is those variables have a memory space somewhere and we're trying to read or write in that portion of memory.
Short answer: just run your example using the -race flag with go run -race
or go build -race
and you'll see a detected data race.
答案3
得分: 1
你的问题的答案是,截至今天,如果a
和b
不大于一个机器字,那么i
必须等于a
或b
。否则,它可能包含一个未指定的值,很可能是从a
和b
的不同部分交错而来的。
截至2022年6月6日的Go内存模型保证,如果程序执行了竞争条件,对于不大于一个机器字的内存访问必须是原子的。
否则,对于一个不大于一个机器字的内存位置x的读取r必须观察到某个写入w,使得r不发生在w之前,并且不存在写入w'使得w发生在w'之前并且w'发生在r之前。也就是说,每次读取必须观察到由前一个或并发的写入写入的值。
这里的happen-before关系在前一节的内存模型中定义。
从较大的内存位置进行竞争性读取的结果是未指定的,但绝对不像C++中那样是未定义的。
对于大于一个机器字的内存位置的读取被鼓励但不要求满足与字大小的内存位置相同的语义,观察到一个允许的写入w。出于性能原因,实现可能将较大的操作视为一组以未指定顺序的单个机器字大小的操作。这意味着对于多字数据结构的竞争可能导致不一致的值,这些值不对应于单个写入。当这些值依赖于内部的一致性(指针、长度)或(指针、类型)对时,这种情况可能发生在接口值、映射、切片和大多数Go实现中的字符串上,这样的竞争反过来可能导致任意的内存损坏。
英文:
The answer to your question, as of today, is that if a
and b
are not larger than a machine word, i
must be equal to a
or b
. Otherwise, it may contains an unspecified value, that is most likely to be an interleave of different parts from a
and b
.
The Go memory model, as of the version on June 6, 2022, guarantees that if a program executes a race condition, a memory access of a location not larger than a machine word must be atomic.
> Otherwise, a read r of a memory location x that is not larger than a machine word must observe some write w such that r does not happen before w and there is no write w' such that w happens before w' and w' happens before r. That is, each read must observe a value written by a preceding or concurrent write.
The happen-before relationship here is defined in the memory model in the previous section.
The result of a racy read from a larger memory location is unspecified, but it is definitely not undefined as in the realm of C++.
> Reads of memory locations larger than a single machine word are encouraged but not required to meet the same semantics as word-sized memory locations, observing a single allowed write w. For performance reasons, implementations may instead treat larger operations as a set of individual machine-word-sized operations in an unspecified order. This means that races on multiword data structures can lead to inconsistent values not corresponding to a single write. When the values depend on the consistency of internal (pointer, length) or (pointer, type) pairs, as can be the case for interface values, maps, slices, and strings in most Go implementations, such races can in turn lead to arbitrary memory corruption.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论