17 | go语句及其执行规则(下)
17 | go语句及其执行规则(下)
讲述:黄洲君
时长09:32大小4.36M
知识扩展
赞 21
提建议
精选留言(76)
- 枫林火山2019-04-01老师,关于顺序打印的demo40.go的优化版本,同来碗绿豆汤同学的实现 package main import "fmt" func main() { var num = 10 sign := make(chan struct{}, 1) for i := 0; i < num; i++ { go func(i int) { fmt.Println(i) sign <- struct{}{} }(i) <-sign } } 这样写为什么不能保证同步,能不能再详细解释下呢。这个实现和您demo39.go 相比,只是合并了两处for循环,我看好多同学也有这个疑问,向您求解。 demo40.go的实现相当于实现了每个异步线程的一个轮询loop。 上面的实现相当于单步间加了一个barrier。执行1->等待->执行2->等待。 实在没理解为什么不能保证同步展开
作者回复: 我又看了一下“来碗绿豆汤”同学写的代码。我可能当时没看清楚,或者没说清楚。 他写的这段代码单从“顺序打印数字”的要求上看是可以的。但是这样做就变成纯同步的流程了,go函数就完全没必要写了。把go函数中的代码拿出来、删掉go函数,再把通道的相关代码也删掉,岂不是更直截了当?像这样: for i := 0; i < num; i++ { fmt.Println(i) } 这个题目的要求是“使得在for循环中启用的多个goroutine按照既定的顺序运行”。你也可以把它理解为“在异步的情况下顺序的打印数字”。所以,“来碗绿豆汤”同学写的代码只满足了其中一个要求,而没有让go函数们自由的异步执行。 我的那个版本demo40.go是让各个go函数(确切地说,是它们调用的trigger函数)自行地检查所需条件,然后再在条件允许的情况下打印数字。这也叫“自旋”。这与纯同步的流程是有本质上的区别的。
共 3 条评论63 - xiao豪2018-09-19回楼上,atomic的加操作和读操作只有32位和64位整数型,所以必须要把int转为intxx。之所以这么做是因为int位数是根据系统决定的,而原子级操作要求速度尽可能的快,所以明确了整数的位数才能最大地提高性能。38
- 来碗绿豆汤2018-09-19我有一个更简单的实现方式, 如下 func main(){ ch := make(chan struct{}) for i:=0; i < 100; i++{ go func(i int){ fmt.Println(i) ch <- struct{}{} }(i) <-ch } } 这样,每次循环都包装goroutine 执行结束才进入下一次循环,就可以保证顺序执行了展开
作者回复: 这些go函数的真正执行谁先谁后是不可控的,所以这样做不行的。
共 9 条评论37 - Geek_3241ef2019-08-26你好,郝老师,请问这里为什么需要sleep呢,我理解的如果不加sleep,其中某个g会一直轮询count的值,当另一个g更改这个值时,那么第一个g就会判断相等才对呀。 但实际上去掉sleep后,程序确实没有按照我理解的逻辑执行,请问这是为什么呢
作者回复: 这主要是因为:Go 调度器在需要的时候只会对正在运行的 goroutine 发出通知,试图让它停下来。但是,它却不会也不能强行让一个 goroutine 停下来。 所以,如果一条 for 语句过于简单的话,比如这里的 for 语句就很简单(因为里面只有一条 if 语句),那么当前的 goroutine 就可能不会去正常响应(或者说没有机会响应)Go 调度器的停止通知。 因此,这里加一个 sleep 是为了:在任何情况下(如任何版本的 Go、任何计算平台下的 Go、任何的 CPU 核心数等),内含这条 for 语句的这些 goroutine 都能够正常地响应停止通知。
15 - Askerlve2018-09-19package main import ( "fmt" "sync/atomic" ) func main() { var count uint32 trigger := func(i uint32, fn func()) { for { if n := atomic.LoadUint32(&count); n == i { fn() atomic.AddUint32(&count, 1) break } } } for i := uint32(0); i < 10; i++ { go func(i uint32) { fn := func() { fmt.Println(i) } trigger(i, fn) }(i) } trigger(10, func() {}) } 测试了下,这个函数的输出不受控,并且好像永远也不会结束,有人能帮忙解释下吗,go小白~😀展开
作者回复: 可以加个sleep
共 7 条评论12 - 老茂2018-10-15不加sleep程序不能正常结束的情况貌似跟cpu核数有关,我是4核cpu,打印0到2每次都可以正常执行;0到3以上就会有卡主的情况,卡主时cpu达到100%,load会超过4。猜测是不是此时所有cpu都在处理count==0的for循环,没有空闲的cpu执行atomic.AddUint32(&count, 1)?
作者回复: Go语言调度goroutine是准抢占式的,虽然会防止某个goroutine运行太久,并做换下处理。但是像简单的死循环这种有可能会换下失败,尤其是windows下,这跟操作系统的底层支持有关。不过一般情况下不用担心。
9 - 言午木杉2020-01-14这篇加了代码,一下子就容易很多了,老师前面的几篇都太多名词了,需要琢磨去好几遍
作者回复: 正式学习一个新东西的首要任务就是“重要名词解析”。一旦熟悉了这些名词以及它们背后的深意,后面的学习效率就会高很多。更重要的是,后面会学得更扎实(或者说很稳)。因为你真正融入了这个新东西所处的世界,站在了它的地基之上。这也是我的一点学习经验。共勉。
共 3 条评论7 - 新垣结裤2018-09-21func main() { num := 10 chs := [num+1]chan struct{}{} for i := 0; i < num+1; i++ { chs[i] = make(chan struct{}) } for i := 0; i < num; i++ { go func(i int) { <- chs[i] fmt.Println(i) chs[i+1] <- struct{}{} }(i) } chs[0] <- struct{}{} <- chs[num] } 每个goroutine执行完通过channel通知下一个goroutine,在主goroutine里控制第一个goroutine的开始,并接收最后一个goroutine结束的信号展开
作者回复: 搞这么多通道有些浪费啊。另外切片不是并发安全的数据类型,最好不要这样用。
8 - Askerlve2018-09-19package main import ( "fmt" "sync/atomic" ) func main() { var count uint32 trigger := func(i uint32, fn func()) { for { if n := atomic.LoadUint32(&count); n == i { fn() atomic.AddUint32(&count, 1) break } } } for i := uint32(0); i < 10; i++ { go func(i uint32) { fn := func() { fmt.Println(i) } trigger(i, fn) }(i) } trigger(10, func() {}) } 这个函数的执行还是不可控诶,并且好像永远也不会结束,是因为我的go版本问题吗?展开
作者回复: Win下可能会有问题,你在bif语句后边加一句time.sleep(time.Nanosecond)。github上的代码我已经更新了。
7 - 肖恩2019-05-08第一遍看好多都看不懂,看到后边回过头来看,发现用自旋goroutine实现,真实奇妙;现在想想,除了文章中实现方式,可以用channel同步实现;还可以用sync.WaitGroup实现
作者回复: 祝贺你升级了;)
6 - 嗷大猫的鱼2018-09-20老师,最近从头学习,前面一直没跟着动手,也没自己总结。这几天在整理每章的重点! https://github.com/wenxuwan/go36 刚写完第二章,突然发现自己动手总结和只看差好多。我会继续保持喜欢总结!
作者回复: 很好,加油!
共 2 条评论6 - 志鑫2019-05-11//个人笔记:使用一个通道来控制 package main import "fmt" func main() { const n = 10 m := 0 ch := make(chan int, 10) //通道长度0~10之间,能够影响性能 for i := 0; i < n; i++ { go func(i int) { for v := <-ch; v != i; v = <-ch { m++ ch <- v //如果不是自己的轮次,则把值再放回去 } fmt.Println(i) ch <- i + 1 }(i) } ch <- 0 for v := <-ch; v != 10; v = <-ch { ch <- v } fmt.Println(m) }展开4
- 传说中的成大大2020-03-19本节内容主要是讲了 goroutine同步那一块 主要通过通道 信号 应该sync包中也会有同步相关的函数 我又去把16讲那个G队列再想了一遍 实际上可能存在多个G队列 G队列之间属于并发关系 但是G队列当中他们又是队列储存 顺序执行的
作者回复: 更确切地说,是顺序地送给某个M执行。不过。它们又可以是并发的,因为如果前一个G进入了等待状态,那么同一个G队列中的后一个G就很可能会获得运行的机会。这时,前一个G并没有运行完成,它可能是在等待计时器到时或者IO操作完成。所以,我们可以说,在这种情况下,前后这两个G就是在并发运行。 进一步讲,同一个G队列中的G都可以如此。所以我们还可以说,Go调度器中的所有可运行G都是有并发运行的能力的,只不过实际上是怎样的,还要看实时调度的具体情况。
共 2 条评论3 - SuperP ❤ 飝2018-10-11runtime.GOMAXPROCS 这个应该能控制P的数量
作者回复: 对,可以。
3 - cygnus2018-09-19demo40的执行结果不是幂等的,程序经常无法正常结束退出,只有极少数几次有正确输出。
作者回复: 你在win下执行的嘛?
3 - sky2018-09-19win64版本:go1.10.2 linux64版本:go1.11 linux下实际运行和预期一样,但为何win下会一直运行不会停止呢,且CPU也已经是100% 表示不解呀
作者回复: 可以加个sleep。
3 - 冰激凌的眼泪2018-09-19‘’否则,我们就先让当前的 goroutine“睡眠”一个纳秒再进入下一个迭代。‘’ 示例代码里没有这个睡眠代码
作者回复: 代码已经更新了。
3 - Geek_d6cfa72021-04-21go 通道传递达到顺序执行目的: func TestTransmitChan(t *testing.T) { start, end := make(chan bool), make(chan bool) head := start for i := 0; i < 10; i++ { end = make(chan bool) temp := i go func(head, end chan bool, i int) { <-head t.Logf("goroutine %v doing", i) end <- true }(head, end, temp) head = end } start <- true <-end t.Logf("main goroutine is end") }展开
作者回复: 变量值的改变也不是并发安全的,所以这样做可能会出问题。而且这个方案有些复杂了。
2 - Pixar2018-09-19func trigger(i int64, fn func()) { for { //if i != 10 { // fmt.Print("") //} if count == i { fn() count += 1 break } } } func main() { for i := int64(0); i <= 9; i++ { go func(i int64) { fn := func() { fmt.Println(i) } trigger(i, fn) }(i) } trigger(10, func() {}) } 取消注释后代码可顺序 0 ~9 输出. 而注释后则会莫名卡主, 怀疑是不是golang runtime 针对这些循环做了些什么, 而且感觉没必要加锁.展开
作者回复: 并发情况下必须利用某种同步工具,否则就不是并发安全的,生产环境中很可能出现不可控的问题。
2 - timmy212018-09-19有一个问题不太清楚,当i和count不相等时,您提到了睡眠1纳秒,可是我没看到有相关的sleep被调用。这是如何做到的?
作者回复: 代码已经更新了,漏掉了。
2