Go语言核心36讲
郝林
《Go并发编程实战》作者,前轻松筹大数据负责人
立即订阅
24094 人已学习
课程目录
已完结 54 讲
0/4登录后,你可以任选4讲全文学习。
开篇词+学习路线 (3讲)
开篇词 | 跟着学,你也能成为Go语言高手
免费
预习篇 | 写给0基础入门的Go语言学习者
50 | 学习专栏的正确姿势
模块一:Go语言基础知识 (6讲)
01 | 工作区和GOPATH
02 | 命令源码文件
03 | 库源码文件
04 | 程序实体的那些事儿(上)
05 | 程序实体的那些事儿(中)
06 | 程序实体的那些事儿 (下)
模块二:Go语言进阶技术 (16讲)
07 | 数组和切片
08 | container包中的那些容器
09 | 字典的操作和约束
10 | 通道的基本操作
11 | 通道的高级玩法
12 | 使用函数的正确姿势
13 | 结构体及其方法的使用法门
14 | 接口类型的合理运用
15 | 关于指针的有限操作
16 | go语句及其执行规则(上)
17 | go语句及其执行规则(下)
18 | if语句、for语句和switch语句
19 | 错误处理(上)
20 | 错误处理 (下)
21 | panic函数、recover函数以及defer语句 (上)
22 | panic函数、recover函数以及defer语句(下)
模块三:Go语言实战与应用 (27讲)
23 | 测试的基本规则和流程 (上)
24 | 测试的基本规则和流程(下)
25 | 更多的测试手法
26 | sync.Mutex与sync.RWMutex
27 | 条件变量sync.Cond (上)
28 | 条件变量sync.Cond (下)
29 | 原子操作(上)
30 | 原子操作(下)
31 | sync.WaitGroup和sync.Once
32 | context.Context类型
33 | 临时对象池sync.Pool
34 | 并发安全字典sync.Map (上)
35 | 并发安全字典sync.Map (下)
36 | unicode与字符编码
37 | strings包与字符串操作
38 | bytes包与字节串操作(上)
39 | bytes包与字节串操作(下)
40 | io包中的接口和工具 (上)
41 | io包中的接口和工具 (下)
42 | bufio包中的数据类型 (上)
43 | bufio包中的数据类型(下)
44 | 使用os包中的API (上)
45 | 使用os包中的API (下)
46 | 访问网络服务
47 | 基于HTTP协议的网络服务
48 | 程序性能分析基础(上)
49 | 程序性能分析基础(下)
尾声与思考题答案 (2讲)
尾声 | 愿你披荆斩棘,所向无敌
新年彩蛋 | 完整版思考题答案
Go语言核心36讲
登录|注册

17 | go语句及其执行规则(下)

郝林 2018-09-19
你好,我是郝林,今天我们继续分享 go 语句执行规则的内容。
在上一篇文章中,我们讲到了 goroutine 在操作系统的并发编程体系,以及在 Go 语言并发编程模型中的地位和作用等一系列内容,今天我们继续来聊一聊这个话题。

知识扩展

问题 1:怎样才能让主 goroutine 等待其他 goroutine?
我刚才说过,一旦主 goroutine 中的代码执行完毕,当前的 Go 程序就会结束运行,无论其他的 goroutine 是否已经在运行了。那么,怎样才能做到等其他的 goroutine 运行完毕之后,再让主 goroutine 结束运行呢?
其实有很多办法可以做到这一点。其中,最简单粗暴的办法就是让主 goroutine“小睡”一会儿。
for i := 0; i < 10; i++ {
go func() {
fmt.Println(i)
}()
}
time.Sleep(time.Millisecond * 500)
for语句的后边,我调用了time包的Sleep函数,并把time.Millisecond * 500的结果作为参数值传给了它。time.Sleep函数的功能就是让当前的 goroutine(在这里就是主 goroutine)暂停运行一段时间,直到到达指定的恢复运行时间。
我们可以把一个相对的时间传给该函数,就像我在这里传入的“500 毫秒”那样。time.Sleep函数会在被调用时用当前的绝对时间,再加上相对时间计算出在未来的恢复运行时间。显然,一旦到达恢复运行时间,当前的 goroutine 就会从“睡眠”中醒来,并开始继续执行后边的代码。
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《Go语言核心36讲》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(41)

  • 来碗绿豆汤
    我有一个更简单的实现方式, 如下
    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函数的真正执行谁先谁后是不可控的,所以这样做不行的。

    2018-09-19
    4
    23
  • xiao豪
    回楼上,atomic的加操作和读操作只有32位和64位整数型,所以必须要把int转为intxx。之所以这么做是因为int位数是根据系统决定的,而原子级操作要求速度尽可能的快,所以明确了整数的位数才能最大地提高性能。
    2018-09-19
    19
  • 枫林火山
    老师,关于顺序打印的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函数)自行地检查所需条件,然后再在条件允许的情况下打印数字。这也叫“自旋”。这与纯同步的流程是有本质上的区别的。

    2019-04-01
    16
  • Askerlve
    package 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

    2018-09-19
    1
    9
  • Askerlve
    package 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上的代码我已经更新了。

    2018-09-19
    5
  • 新垣结裤
    func 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结束的信号

    作者回复: 搞这么多通道有些浪费啊。另外切片不是并发安全的数据类型,最好不要这样用。

    2018-09-21
    4
  • 嗷大猫的鱼
    老师,最近从头学习,前面一直没跟着动手,也没自己总结。这几天在整理每章的重点!

    https://github.com/wenxuwan/go36

    刚写完第二章,突然发现自己动手总结和只看差好多。我会继续保持喜欢总结!

    作者回复: 很好,加油!

    2018-09-20
    4
  • 老茂
    不加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下,这跟操作系统的底层支持有关。不过一般情况下不用担心。

    2018-10-15
    3
  • cygnus
    demo40的执行结果不是幂等的,程序经常无法正常结束退出,只有极少数几次有正确输出。

    作者回复: 你在win下执行的嘛?

    2018-09-19
    2
  • sky
    win64版本:go1.10.2
    linux64版本:go1.11

    linux下实际运行和预期一样,但为何win下会一直运行不会停止呢,且CPU也已经是100% 表示不解呀

    作者回复: 可以加个sleep。

    2018-09-19
    2
  • 冰激凌的眼泪
    ‘’否则,我们就先让当前的 goroutine“睡眠”一个纳秒再进入下一个迭代。‘’

    示例代码里没有这个睡眠代码

    作者回复: 代码已经更新了。

    2018-09-19
    2
  • 志鑫
    //个人笔记:使用一个通道来控制
    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)
    }
    2019-05-11
    1
  • 肖恩
    第一遍看好多都看不懂,看到后边回过头来看,发现用自旋goroutine实现,真实奇妙;现在想想,除了文章中实现方式,可以用channel同步实现;还可以用sync.WaitGroup实现

    作者回复: 祝贺你升级了;)

    2019-05-08
    1
  • Wind
    func main(){
        ch := make(chan struct{})
        for i:=0; i < 100; i++{
            go func(i int){
                fmt.Println(i)
                ch <- struct{}{}
            }(i)
            <-ch
        }
    }
    亲测可以,而且循环的前提是必须上一个通道不阻塞的情况下才能继续,这样的确可以保证按顺序输出
    2019-03-05
    1
  • SuperP ❤ 飝
    runtime.GOMAXPROCS 这个应该能控制P的数量

    作者回复: 对,可以。

    2018-10-11
    1
  • timmy21
    有一个问题不太清楚,当i和count不相等时,您提到了睡眠1纳秒,可是我没看到有相关的sleep被调用。这是如何做到的?

    作者回复: 代码已经更新了,漏掉了。

    2018-09-19
    1
  • prader
    可以通过striger函数,控制goroutine的执行顺序。
    2019-11-02
  • Jee
    runtime.GOMAXPROCS runtime.NumCPU runtime.NumGoroutine
    2019-10-24
  • Geek_3241ef
    你好,郝老师,请问这里为什么需要sleep呢,我理解的如果不加sleep,其中某个g会一直轮询count的值,当另一个g更改这个值时,那么第一个g就会判断相等才对呀。
    但实际上去掉sleep后,程序确实没有按照我理解的逻辑执行,请问这是为什么呢

    作者回复: 这主要是因为:Go 调度器在需要的时候只会对正在运行的 goroutine 发出通知,试图让它停下来。但是,它却不会也不能强行让一个 goroutine 停下来。

    所以,如果一条 for 语句过于简单的话,比如这里的 for 语句就很简单(因为里面只有一条 if 语句),那么当前的 goroutine 就可能不会去正常响应(或者说没有机会响应)Go 调度器的停止通知。

    因此,这里加一个 sleep 是为了:在任何情况下(如任何版本的 Go、任何计算平台下的 Go、任何的 CPU 核心数等),内含这条 for 语句的这些 goroutine 都能够正常地响应停止通知。

    2019-08-26
  • 黄伟洪
    demo40.go 我注释了time.Sleep(time.Nanosecond)后,程序就停着一直不动了。。。。。想不明白为什么

    作者回复: 在某些计算平台下,过于简单的 for 语句有可能会造成 CPU 时间片无法让出的问题。

    2019-08-04
收起评论
41
返回
顶部