- 介绍
go内存模型是指在特定的条件下,向goroutine中的变量写入值,在另一个goroutine中能够读取到该变量的值
- 建议
多个goroute同时修改一个数据必须是有序的
使用channel或sync、sync/atomic包中提供的同步原语,可保证对数据顺序访问
- happens before
单个goroutine读写必需按一定顺序执行,编译器和处理器只有在不改变程序执行最终结果的情况下会对单个goroutine的读写重新排序。重新排序会导致一个goroutine看到的行为与另一个goroutine不一致。比如在一个goroutine中执行a=1;b=2,另外的goroutine看到的b值的更新发生在a值更新之前。
golang的内存操作读写请求的happens before:事件e1 happens before 事件e2,则e2在e1后执行。事件e1不是happen before 事件e2且不是在事件e2之后执行,则说事件e1和事件e2是同时执行的。单个goroutine的happens-before是由程序顺序表达。
以下两条件都满足时一个写操作w向变量v写入数据,读操作r可以观察到变量v的值
1、读操作r没有 happen before 写操作w
2、在写w操作之后与读r操作之前,没有其他的写操作w’’对变量v写入数据为了保证读操作r读到变量v,是写操作w向变量v写入的值,必须符合以下两个条件
1、写操作w happens before 读操作r
2、其他的写变量V的操作要么发生在写操作w之前,要么发生在读操作r之后这部分限制条件比第一种限制条件严格,要求没有其他写操作同时与写操作w或读操作r一起执行。
单个goroutine没有并发问题,写操作w的变量v可以被读操作r读取到。当多个goroutines访问共享变量v,必须使用同步事件建立执行顺序,确保写操作的变量值被读操作正确读取。
在内存模型中将初始化变量v类型零值的行为作为一次写操作。
读写值大于32位(4 bytes)或64位操作系统(8 bytes)时的操作行为,跟在多个32位或64位操作系统操作的行为顺序一致是不明确的。
- 同步
Initialization
程序初始化运行单个goroutine,但这个goroutine可能创建其他同时运行的goroutines。
package p导入package q,q的init方法比p的先执行。
main方法在所有init方法完成初始化后执行
goroutine creation
go声明启动一个goroutine happens before 执行goroutine,示例如下:
var a string
func f() {
print(a)
}
func hello() {
a = "hello, world"
go f()
}
未来可能执行hello方法可能会打印出”hello, world”,目前不会
goroutine destruction
一个goroutine的退出不保证任何事件的执行顺序,示例
var a string
func hello() {
go func() { a = "hello" }()
print(a)
}
给变量a赋值不是一个同步事件,所以不能保证变量a可以被其他goroutine获取到。这段a的赋值操作代码可能在程序编译阶段直接被丢弃掉了。
假如一个goroutine变量值对另一个goroutine可获取到,需要使用锁或信道(channel)通信同步机制保证执行顺序。
Channel communication
goroutines之间的消息同步的主要是通过信道通信(channel)方式实现。在不同的groutine中,每个发送的信道,有个对应的接收信道与其对应。
发送信道happens before接收信道。示例:
var c = make(chan int, 10)
var a string
func f() {
a = "hello, world"
c <- 0
}
func main() {
go f()
<-c
print(a)
}
为保证输出“hello, world”,写入变量a happens before发送信息至信道c,发送信道happens before从接收信道c中获取信息,接收信息happens before打印动作print.
一个关闭的信道happens before 接收信道,未向信道中发送信息,会返回一个零值,因为信道已经关闭了。可通过替换代码中c<- 0为close(c)代码输出结果是一致的。
下面这段代码类似,使用无缓存的信道通信且变换下发送与接收语句
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获取数据,从接收信道获取数据happens before从信道c中发送数据,从信道c中发送数据happens before 打印动作Print
假如使用的是缓存信道(如c = make(chan int, 1)),这段代码不能保证输出“hello, world”。代码可能输出空的字符串,崩溃,或其他。
向一个容量为C的信道发送数据,从该信道接收数据时第k个数据接收happens before 第k+c个数据接收
总结缓存信道通信规则。缓存信道通信方式允许统计信号量:信号量数为活跃使用数,信号量容量为最大活跃使用数,发送数据请求信号量,接收数据释放信号量。这是用于限制并发的常用方法。
下列代码,为work启动一个goroutine,使用了信道限制确保同一时刻最多有三个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两种数据类型的锁。
变量l为sync.Mutex or sync.RWMutex锁,假如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”。在f方法中第一次调用l.Unlock()返回happens before在main方法中第二次调用l.Lock()返回,可正常打印输出变量a的值
变量l为 sync.RWMutex调用 l.RLock时,n调用l.RLock 返回happens after n调用l.Unlock,同时l.RUnlock happens before n+1调用l.Lock
Once在多个groroutine中sync包使用Once类型安全初始化,多个线程同时执行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可以观察到写happened before写操作w的值
示例:
var a, b int
func f() {
a = 1
b = 2
}
func g() {
print(b)
print(a)
}
func main() {
go f()
g()
}
代码可能发生执行方法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)
}
跟前面的例子一样,这段代码也有可能输出空字符串,观察写数据至变量done,意味着观察写数据至变量a。在两个线程中,没有同步事件用于保证main方法可以观察到写入done变量的值,不能保证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然后退出循环,还是不能保证main方法可以观察到g.msg的初始化值。
所有的这些错误的例子,表明要从一个gorotine观察到另一个goroutine赋值问题必须使用显式的同步机制。
关联的部分面试题目
该程序片段输出内容是什么?这种问法是否有误,是否换应该换种思路问:如要输出正确的i值应该怎么处理?
func main() {
runtime.GOMAXPROCS(1)
wg := sync.WaitGroup{}
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
fmt.Println("A: ", i)
wg.Done()
}()
}
wg.Wait()
}
该程序片断使用是否有问题?如有如何纠正?
func goRoutineA(a <- chan int) {
val := <- a
fmt.Println("goRoutineA received the data", val)
}
func goRoutineB(b <- chan int) {
val := <- b
fmt.Println("goRoutineB received the data", val)
}
func main() {
ch := make(chan int)
go goRoutineA(ch)
go goRoutineB(ch)
ch <- 3
time.Sleep(time.Second * 1)
}
来源:51CTO
作者:qwjhq
链接:https://blog.51cto.com/bingdian/2483212