内容简介:多个goroute同时修改一个数据必须是有序的使用channel或sync、sync/atomic包中提供的同步原语,可保证对数据顺序访问Initialization
- 介绍
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赋值问题必须使用显式的同步机制。
来源: https://golang.org/ref/mem
关联的部分面试题目
该程序片段输出内容是什么?这种问法是否有误,是否换应该换种思路问:如要输出正确的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) }
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- JVM内存模型 与 JMM内存模型
- C++11 中的内存模型(上):内存模型基础
- C++11 中的内存模型(下):C++11 支持的几种内存模型
- JVM内存模型解析
- 并发编程:内存模型
- 理解Java内存模型
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
驾驭未来:抓住奇点冲击下的商机
[日]斋藤和纪 / 南浩洁 / 中国友谊出版公司 / 2018-9 / 52.00元
2020年左右,AI(人工智能)将超越人类的智力水平。2045年,人类将迎来“奇点”——科技进步的速度达到无限大。 所有技术都在以空前的速度向前发展。同时,以往带来巨大财富的众多技术将走向“非货币化”。当下,人类正面临着被AI夺去工作的危机。许多传统行业(例如汽车制造业)将被彻底颠覆,但新的机会也在酝酿,技术的进步使得带宽成本、计算成本、存储成本等创新成本趋近于0,创业不再是资本、技术或信息......一起来看看 《驾驭未来:抓住奇点冲击下的商机》 这本书的介绍吧!