Skip to the content.

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.Mutexsync.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同步

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.