The Go Memory Model
1.channel
当buffer channel的空间被填满后,它的表现就和unbuffer channel一样了。
1.1 channel通信(带缓冲的channel)
channel的通信可以视为goruntine之间同步的主要手段。在不同的goruntine中,特定的channelsend都和对应的receive相匹配。(这里的主体是channel,channel的send是指 <-c ,receive是指 c<-a )
var c = make(chan int, 10) // 提前预置了10个int大小的缓冲
var a string
func f() {
a = "hello, world"
c <- 0
}
func main() {
go f()
<-c
print(a)
}
使用了带缓冲的channel的goroutines可以确保print(a)为我们期望的字符串,这里的执行顺序应该是:
a=”hello,world”→ c<-0→<-c→print(a)
带缓冲的通道,已经留出了固定的容量,因此在receive操作前,send操作都必须阻塞(没有数据可以给外界),因此这里可以确保a的赋值操作可以在打印操作之前执行;与DB中的serializability道理是相似的,正确执行结果可以序列化为某种严格顺序执行的结果。
此外,在缓冲为C的channel上,第k+C次send操作应该在第k次receive操作之前被同步完成。不难理解,若做不到这点,那么receive的packet会一直阻塞等待channel的空间释放。
1.2 channel通信(不带缓冲的channel)
var c = make(chan int) // 未提前设置缓冲
var a string
func f() {
a = "hello, world"
<-c
}
func main() {
go f()
c <- 0
print(a)
}
同理,使用了不带缓冲的channel也可以确保打印出赋予a的字符串,这里的执行顺序应该是:
a=”hello,world”→ <-c→c<-0→print(a)
因为这里是不带缓冲的channel,因此在send操作前,receive操作都必须阻塞(不知道是否有容量可以容纳自己发送的数据,因此认为自己是满的),待send操作后,channel知道自己有一个空间空闲了,receive可以继续,因此可以确保按照上述的执行顺序执行。
1.3 带缓冲的channel限制并行
var limit = make(chan int, 3)
func main() {
for _, w := range work {
go func(w func()) {
limit <- 1
w()
<-limit
}(w)
}
select{}
}
以上程序限制了在同一时间内最多有3个goroutine可以同时运行。
2.Locks
Go的sync包实现了两种数据锁:sync.Mutex和sync.RWMutex 两种锁,看名字就知道,前者是普通的互斥锁,后者是读写锁。
var l sync.Mutex
var a string
func f() {
a = "hello, world"
l.Unlock()
}
func main() {
l.Lock()
go f()
l.Lock()
print(a)
}
此程序可以确保a = “hello world”的内容可以被打印出来,这里的Mutex用法和C++一样。
3.Once
sync包通过使用Once类型提供了在存在多个goroutines时进行初始化的安全机制。多个线程可以为特定的f执行 Once.Do (f),但只有一个线程将运行f(),其他调用将阻塞直到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()
}
调用两次doprint只会调用setup一次。setup函数将在任何一次打印调用之前完成。结果是“hello, world”将被打印两次。
4. Atomic Values
sync/atomicpackage中的api统称为“原子操作”,可用于同步不同goroutine的执行。如果一个原子操作A的效果被原子操作B观察到,那么A就会在B之前同步。程序中执行的所有原子操作都是按照某种顺序一致的顺序执行的。前面的定义与c++的顺序一致原子和Java的volatile变量具有相同的语义。
5.Additional Mechanisms
Thesyncpackage提供了额外的同步抽象,包括
condition variablesgo.dev/pkg/sync/#Cond
lock-free mapsgo.dev/pkg/sync/#Map
allocation poolsgo.dev/pkg/sync/#Pool
wait groupsgo.dev/pkg/sync/#WaitGroup
其中每一个的文档都指定了关于同步的保证。其他提供同步抽象的包也应该记录它们所做的保证。后续有需要可以具体的学习如何使用。
6.阅读心得
阅读完关于Incorrect synchronization和Incorrect compilation这两部分后,其实他们本质上在说的东西都是相同的,这里记录个人见解如下:
6.1 Go同步
- Go的同步本质也是serializablity,使执行可以序列化,目的是使某些操作必然可以发生在一些操作之前
- 在goroutine的大背景下,且没有同步原语的存在,那么写的代码的先后顺序不能代表真正的执行顺序。如:w’->w->r->r’,这种代码编写顺序下,在goroutine情况下,且不用相关的同步原语,他们的顺序已经被完全的打乱了,我们不能保证r’一定可以看到w’的内容。
6.2 Go编译
Go的编译器在编译时,不可以像C++那样修改语句顺序(结果等价修改),在Go中,衡量程序修改正确、错误的一个标准就是,比如程序中有一个变量a,我们修改完后,我们在程序运行周期之间看到的a的所有可能的值是不能发生变化的,若发生变化,则说明这种修改是错误的。
*p = i + *p/2
// 改写后
*p /= 2
*p += i
这种改写是错误的,虽然结果不变,但是:如果i和p开始时等于2,原始代码将p =3,因此一个正在运行的线程只能从p读取2或3。重写后的代码先执行p =1,然后执行*p = 3,允许正在运行的线程读取1和3。
改写后*p的可能值发生了变化,因此这种改写是不被允许的。我们应该确保我们的程序中没有data-race。
back.