https://go.dev/ref/mem The Go Memory Model 整理学习。
Go 内存模型规定了,在一个 goroutine 中读取一个变量时,可以保证观察到不同 goroutine 中对同一变量的写入所产生的值的条件。
数据竞争:对一个内存位置的写与对同一位置的另一个读或写同时发生,除非所有涉及的访问都是同步/原子包提供的原子数据访问。
内存操作由四个细节描述:
要求 1:每个 goroutine 中的内存操作必须对应于该 goroutine 的正确顺序执行,考虑从内存中读取和写入的值。该执行必须与 sequenced before 关系一致,该关系定义为 Go 语言规范对于 Go 控制流构造的部分顺序要求以及表达式的求值顺序。
【Go 程序执行被建模为一组 goroutine 执行,以及一个映射 W,指定每个类似读取的操作从哪个类似写入的操作中读取。】
要求 2:对于给定的程序执行,当限制为同步操作时,映射 W 必须可以通过一些隐含的同步操作的总序来解释,该总序与这些操作读取和写入的值以及顺序一致。
synchronized before 关系是从 W 派生的同步内存操作的偏序关系。如果同步读取类内存操作 r 观察到同步写入类内存操作 w(即,如果 W(r) = w),则 w 在 r 之前同步。
happens before 关系被定义为 sequenced before 关系和 synchronized before 的并集的传递闭包。
要求 3:对于内存位置 x 上的普通(非同步)数据读取 r,W(r) 必须是对 r 可见的写入 w,其中“可见”意味着以下两个条件都成立:
任何实现都可以在检测到数据竞争时报告该竞争并停止程序的执行。使用 ThreadSanitizer(通过 go build -race
访问)的实现正是这样做的。
init函数
是 happen before p 包任何函数;init 函数
的完成是 synchronized before 主包中的 main.main 函数执行。创建新 goroutine 的 go 语句 synchronized before 这个 goroutine 的执行开始。
不能保证 goroutine 的退出 synchronized before 于程序中的任意事件。
通道通信是 goroutine 之间同步的主要方法。一个特定通道上的每一个发送都与该通道的相应接收相匹配,通常是在一个不同的 goroutine 中。
var c = make(chan int, 10)
var a string
func f() {
a = "hello, world"
c <- 0 // close(c)
}
func main() {
go f()
<-c
print(a)
}
例子保证会打印 "hello world"
。对 a
的写是 sequence before 于 c
的发送,c
的发送是 synchronize before 于对应(int) c
的接收,而 c
的接收 sequence before 于打印。
var c = make(chan int)
var a string
func f() {
a = "hello, world"
<-c
}
func main() {
go f()
c <- 0
print(a)
}
var limit = make(chan int, 3)
func main() {
for _, w := range work {
go func(w func()) {
limit <- 1
w()
<-limit
}(w)
}
select{} // 一直阻塞
}
sync package
实现了两种锁 lock 数据类型:sync.Mutex
以及 sync.RWMutex
。
sync.Mutex
或 sync.RWMutex
变量 l
,并且 ,前 n 次调用 l.Unlock()
完成是 synchronized before 于第 m 次 l.Lock()
返回。sync.RWMutex
变量 l
的 l.RLock()
调用,都存在一个 n,使得第 n 次调用 l.Unlock()
是 synchronized before 于 l.RLock()
返回,并且对应的 l.RUnlock()
调用完成 synchronized before 于第 n+1 次对 l.Lock()
的调用返回。l.TryLock
(或 l.TryRLock
)可以被认为是能够返回 false
的,即使是在 mutex l
是解锁状态。】sync package
通过 Once
类型为存在多 goroutine 情况下的初始化提供了一种安全机制。
多个线程为函数 f 可执行 once.Do(f)
,只有一个 goroutine 执行 f 函数,其它 goroutine 会阻塞直到该 f 函数返回。
【使用 done
标志和互斥锁实现 】
once.Do(f)
中该单次 f 调用完成是 synchronized before 于任何 once.Do(f)
调用的返回。var a string
var once sync.Once
func setup() {
a = "hello, world"
}
func doprint() {
once.Do(setup)
print(a)
}
func twoprint() {
go doprint()
go doprint()
}
// "hello, world" will be printed twice.
sync/atomic 包中的 API 是“原子操作”,可用于同步不同 goroutine 的执行。如果原子操作 A 的效果被原子操作 B 观察到,则 A 在 B 之前同步。程序中执行的所有原子操作都表现为按某种顺序连续执行的顺序一致。这个定义与 C++的顺序一致原子(变量)和 Java 的 volatile 变量具有相同的语义。
在 Go 语言中,Finalizer 是一种机制,用于在对象被垃圾回收器回收之前执行一些清理操作。Finalizer 是通过 runtime.SetFinalizer
函数设置的,该函数接受两个参数:要设置 Finalizer 的对象和一个函数,该函数将在对象被垃圾回收器回收之前执行。
当一个对象被创建时,Go 语言的垃圾回收器会为其分配内存,并将其标记为“可达”。如果该对象不再被引用,垃圾回收器会将其标记为“不可达”,并在某个时间点回收该对象的内存。在回收对象之前,垃圾回收器会检查该对象是否有 Finalizer。如果有,垃圾回收器会将该对象添加到一个 Finalizer 队列中,并在下一次垃圾回收时执行 Finalizer 函数。
另外,由于 Finalizer 函数的执行是不确定的(甚至可能不执行),因此不建议在程序中过多地使用 Finalizer。如果需要释放资源或执行必须的清理操作,应该使用 defer 语句或显式地调用清理函数来实现。
SetFinalizer(x, f)
是 synchronized before 函数 f(x) 的调用。condition variables, lock-free maps, allocation pools, and wait groups.
程序中存在竞争条件时,可能会出现非顺序一致的执行结果。特别地,一个读操作 r 可能会观察到与 r 并发执行的任何写操作 w 所写入的值。即使这种情况发生了,也不能保证在 w 之前发生的写操作会被在 r 之后的读操作所观察到。
var a, b int
func f() {
a = 1
b = 2
}
func g() {
print(b)
print(a)
}
func main() {
go f()
g()
}
g() 可能打印 2 和 0 (f 函数两个写语句不满足 sequenced before 关系
另一个例子,不正确的忙等待:
var a string
var done bool
func setup() {
a = "hello, world"
done = true
}
func main() {
go setup()
for !done {
}
print(a)
}
可能打印空字符串,main 中观察到写入 done,并不保证已经观察到写入 a。
更糟糕的是:由于两个线程之间没有同步事件,所以也无法保证写入 done 的内容会被 main 观察到,不能保证 main 中的循环能够结束。
需要注意上述问题的解决方法:使用显示的同步语句。
Go 语言内存模型对编译器优化的限制,一些编译器优化在单线程程序中是有效的,但不是对于所有 Go 程序都是有效的。特别是,编译器不能引入源程序中不存在的写入操作,不允许使得单个读取操作观察到多个不同的值,不允许使得单个写操作写入多个值。【主要目的,避免编译器在优化程序时引入数据竞争以及不确定的行为。】
在没有竞争条件的程序中,不应该将写操作从它们所在的条件语句中移出。(避免其它 goroutine 观察可能观察到原来不可能出现的值)
在没有竞争条件的程序中,编译器不应该假设循环一定会终止。
在没有竞争条件的程序中,编译器不应该假设被调用的函数总是会返回或不包含同步操作。
编译器不允许单个读操作观察到多个值,这意味着编译器不应该从共享内存中重新加载本地变量。
i := *p
if i<0 || i>=len(funcs){...}
funcs[i]()
// 禁止重新加载 *p
funcs[*p]()
*p = i + *p/2 // if i=2,*p=2, 只能观察到 *p 为 2/3
// 不能改写为 2/1/3
*p /=2
*p += i
说明:所有这些优化在 C/C++编译器中都是允许的,但是与 C/C++编译器共享后端的 Go 编译器必须注意禁用那些对 Go 无效的优化。
请注意,禁止引入数据竞争的规则不适用于编译器可以证明这些竞争不会影响目标平台上的正确执行的情况。
本文链接: Go 内存模型
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
发布日期: 2023-04-04
最新构建: 2024-12-26
欢迎任何与文章内容相关并保持尊重的评论😊 !