go内存模型(二)
这是我参与2022首次更文挑战的第11天,活动详情查看:2022首次更文挑战
原文链接:golang.org/ref/mem
Channel communication(管道通信)
管道通信是goroutines之间同步的主要方法。每一个发送到一个特别的管道会被匹配给一个相应的不同的goroutine从管道中接收。
一个管道的发送 happens before管道完成接收。
下面这个程序:保证会运行"hello, world"。
var c = make(chan int, 10)
var a string
func f() {
a = "hello,world"
c <- 0
}
func main() {
go f()
<-c
print(a)
}
对a的写 happens before 于c的发送,c的发送又happens before于c完成接收,c的接收也happens before于print函数。
一个channel的关闭happens before于接收返回0值,因为channel被关闭了。
在前面的例子中,把c<-0替换成 close©产出的程序拥有相同的保证行为。
从一个非缓冲channel的接收 happens before于channel完成时的发送。
这一个程序(跟上面一样,但是发送和接收交换,并且使用了一个非缓冲的channel):
var c = make(chan int)
var a string
func f() {
a = "hello, world"
<-c
}
func main() {
go f()
c <- 0
print(a)
}
也保证会打印"hello, world"。对a的写 happens before于对c的接收,c的接收happens before于相应的c的完成时的发送,c的完成时的发送happens before于print。
如果channel是缓冲的(e.g. c = make(chan int, 1)),这样的话程序将不会保证打印"hello, world"(可能会打印空的字符串或者其他东西。)
一个容量为c的channel的第k个接收,happens before于第k+c个channel完成时的发送。
这个规则概括了缓冲channel的前置规则。它允许一个计数信号量通过一个缓冲channel来建模:channel中的项目数量相当于活跃使用的数量,channel的容量相当于同时使用的最大数量,发送一个项目获得信号量,然后接受一个项目释放这个信号量。这是一个用于限制并发的惯用语。
这个程序为work list中的每一个元素开启一个goroutine,但是这些goroutines使用limit channel来协调,确保最多同时只有三个运行中的work函数。
var limit = make(chan int, 3)
func main() {
for _, w := range work {
go func(w func()) {
limit <- 1
w()
<-limit
}(w)
}
select{}
}
Locks
sync包实现了两个锁数据类型,sync.Mutex和sync.RWMutex.
对任何sync.Mutex或者sync.RWMutex的变量l来说,n < m,调用n的l.Unlock() happens before m 的l.Lock()返回。
var l sync.Mutex
var a string
func f() {
a = "hello, world"
l.Unlock()
}
func main() {
l.Lock()
go f()
l.Lock()
print(a)
}
这个程序保证打印"hello, world"。第一个调用l.Unlock() happens before 于第二个l.Lock()(main中)的返回,这个返回happens before 于print函数。
对任何调用l.RLock() 在一个sync.RWMutex变量l,有一个n,使得l.RLock在调用n到l.Unlock之后发生(返回),而匹配的l.RUnlock在调用n+1到l.Lock之前发生。
Once sync包提供了一个安全机制来初始化,在有多个goroutines时通过使用Once类型。多线程可以为一个指定的f函数执行once.Do(f),但是只要有一个执行f()函数,其他的调用会被阻塞直到f() 已经返回。
从once.Do(f)的一个简单f() 的调用 happens(返回) 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()
}
在这个程序中,调用twoprint方法只会执行一个setup。setup 函数将会甚至在print的调用之前完成。结果是"hello, world"将会被打印两次。
Incorrect synchronization 注意,一个读r可能会观察到被一个跟r并发发生的写w 写过的值。即便这发生了,也不意味着读 happening after r 会观察到happen before w的写。
var a, b int
func f() {
a = 1
b = 2
}
func g() {
print(b)
print(a)
}
func main() {
go f()
g()
}
这个程序将会打印2和0
这个事实让一些通用的惯用语失效了。
双重校验锁是一种为了避免同步开销的尝试。例如,这个twoprint可能被不正确地写成:
var a string
var done bool
func setup() {
a = "hello, world"
done = true
}
func doprint() {
if !done {
once.Do(setup)
}
print(a)
}
func twoprint() {
go doprint()
go doprint()
}
但是没有办法保证的是,在doprint,观察到对done的写意味着观察到对a的写。这个版本会打印一个空的字符串而不是"hello, world".
另一个不正确的惯用是忙等一个值,就像这里面:
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中的循环无法保证完成。
在这个主题上还有更精妙的变形,例如这个程序。
type T struct {
msg string
}
var g *T
func setup() {
t := new(T)
t.msg = "hello, world"
g = t
}
func main() {
go setup()
for g == nil {
}
print(g.msg)
}
即使main观察到g!=nil并且推出勋魂,也没法保证它将会观察到g.msg的初始化。
在这些所有的例子中,解决方案是相同的:使用明确的同步机制。
转载自:https://juejin.cn/post/7059595757745078303