一个能在串行程序中正确工作的函数。如果这个函数在并发调用时仍然能正确工作,那么这个函数是并发安全的。在这里并发调用是指,在没有额外同步机制的情况下,从两个或者多个 goroutine 同时调用这个函数。如果一个类型的所有可访问方法和操作都是并发安全时,则它可称为并发安全的类型。
数据竟态发生于两个 goroutine 并发读写同一个变量,并且至少一个是写入时。有3个方法避免数据竟态。
第一种方法是不要修改变量。
第二种方法是避免多个 goroutine 访问同一个变量。
第三种方法是允许多个 goroutine 访问同一个变量,但是在同一时间只有一个 goroutine 可以访问,这种方法称为互斥机制。
在 Mutex 的 Lock 和 Unlock 之间,可能自由地读取和修改共享变量,这一部分称为临界区域。
仅在绝大部分 goroutine 都在获取读锁并且锁竞争比较激烈时,RWMutex 才有优势。
现代计算机一般会有多个处理器,每个处理器都有内存的本地缓存。为了提高效率,对内存的写入是缓存在每个处理器中的,只有在必要时才刷回内存。甚至刷回内存的顺序都可能与 goroutine 的写入顺序不一致。像通道或者互斥锁操作这样的同步原语都会使处理器把积累的写操作刷回内存并提交。
var loadIconsOnce sync.Once
var icons map[string]image.Image
// 并发安全
func Icon(name string) image.Image {
loadiconsOnce.Do(loadIcons)
return icons[name]
}
把 -race 命令行添加到 go build, go run, go test 命令里面即可以使用该功能。
每个 OS 线程都有一个固定大小的栈内存(通常为 2MB),栈内存区域用于保存在其他函数调用期间哪些正在执行或临时暂停函数中的局部变量。这个固定的栈大小既又太大又太小。对于一个小的 goroutine,2MB的栈是一个巨大的浪费。对于复杂的和深度递归的函数,固定大小的栈始终不够大。
作为对比,一个 goroutine 在生命周期开始只有一个很小的栈,典型情况下为 2KB。与 OS 线程不同的是,goroutine 的栈不是固定大小的,它可以按需增大和缩小。goroutine 的栈可以到达 1GB,比线程典型的固定大小栈高出几个数量级。
Go 运行时包含一个自己的调度器,这个调度器使用一个称为 m:n 调度的技术(因为它可以复用/调度 m 个 goroutine 到 n 个 OS 线程)。Go 调度器与内核调度器的工作类似,但只关心单个 Go 程序的 goroutine 调度问题。
Go 调度器使用 GOMAXPROCS 参数确定使用多少个 OS 线程来同时执行 Go 代码。默认值是机器上 CPU 的数量。
GOMAXPROCS=1 go run hack-cliche.go
所以没有线程本地变量之类的结构。