美文网首页
关于goroutine的一个简单案例的思考

关于goroutine的一个简单案例的思考

作者: leejnull | 来源:发表于2020-01-08 22:28 被阅读0次

首先思考这样一个问题

下面这段代码会输出什么?

func main() {
    for i := 0; i < 10; i++ {
        go func() {
            fmt.Println(i)
        }()
    }
}

估计都不会说是 1,2,3,4...10的吧😂

我们来看一下输出

lijun:class6/ $ go run class6_demo.go                         [17:05:23]
lijun:class6/ $      

答案是什么都没有

我的思考过程

我依稀记得上周看Go入门的时候讲到了这个例子(这里吐槽一下之前只了解了大概,到今天才算是明白其中的究竟)
所以我凭着仅有的记忆修改了一下

func main() {
    for i := 0; i < 10; i++ {
        go func(a int) {
            fmt.Println(a)
        }(i)
    }
}

为什么要这么改呢?因为当时就记住了如果要正确输出 1,2,3...10 的话,得传个参数进去,不然结果就会是 10,10,10...10(这个点下面会讲到),我们再来看一下输出

lijun:class6/ $ go run class6_demo.go                                                             [17:05:26]
lijun:class6/ $  

还是没有。
再加一句这个

func main() {
    for i := 0; i < 10; i++ {
        go func(a int) {
            fmt.Println(a)
        }(i)
    }
    time.Sleep(time.Second*1)
}

打印一下输出

lijun:class6/ $ go run class6_demo.go                                                             [17:11:34]
5
1
0
7
8
6
9
3
4
2

有了!为什么会这样?我记得之前学 Python 的时候,在用多线程的时候也碰到了这样的情况,当时举得例子都差不多,要么用时间函数延迟 1s,要么加到守护线程了。不然子线程会随着主线程的执行结束而全部销毁,就什么都不干了。

所以结论

会出现一开始什么都不输出的情况是主线程结束运行了,但是 goroutine 函数还没有执行!

为什么会没有执行呢?

这里我们要理解一下 goroutine 的概念

Don’t communicate by sharing memory; share memory by communicating.

这是 Go 语言的核心所在:不要通过共享内存来通讯,而应该通过通讯来共享内存

channel+goroutine 组成了Go语言中的并发编程模式(有别于其他语言的多线程)
其中 goroutine 就代表着并发编程模型中的用户级线程

所谓的用户级线程指的是架设在系统级线程之上的,由用户(或者说我们编写的程序)完全控制的代码执行流程。用户级线程的创建、销毁、调度、状态变更以及其中的代码和数据都完全需要我们的程序自己去实现和处理。这带来了很多优势,比如,因为它们的创建和销毁并不用通过操作系统去做,所以速度会很快,由于不用等着操作系统去调度它们的运行,所以往往会很容易控制并且可以很灵活。

与之对应的就是系统级线程。在 Go 语言当中,运行时(runtime)系统会帮助我们自动创建和销毁系统级线程(由操作系统提供)。

用户级线程相比于系统级线程,最明显也最重要的一个劣势就是复杂。如果我们只使用了系统级线程,那么我们只要指明需要新线程执行的代码片段,并且下达创建或销毁线程的指令就好了,其他的一切具体实现都会由操作系统代劳。
但是使用用户级线程的话,既是指令下达者,又是指令执行者。我们必须全权负责与用户级线程有关的所有具体实现。
操作系统不但不会帮忙,还会要求我们的具体实现必须与它正确地对接,否则用户级线程就无法被并发地,甚至正确地运行。毕竟我们编写的所有代码最终都需要通过操作系统才能在计算机上执行

幸好 Go 语言提供了一个强大的用于调度 goroutine、对接系统级线程的调度器。
它主要负责统筹调配 Go 并发编程模型中的三个主要元素,即:G(goroutine 的缩写)、P(processor 的缩写)和 M(machine 的缩写)。
M指代的就是系统级线程,多线程的目的就是合理的分配调度多核CPU的资源,所以三者的关系如下图所示


2019-08-23-01.png

系统会有一个 goroutine 队列,每一个 goroutine 函数都会把它放到这个队列中,由 M 分配到的 P 来一个一个处理 G,如果处理的 G 是耗时很长(如I/O操作或锁的接触)的话,这个 G 就会放在当前的 M 里面休眠,等待它执行完毕之后再唤醒放到队列中继续等待被分配 M 和 P 执行,此时 P 就会转到其他的 M 中继续从队列中获取 G 执行


2019-08-23-02.png
另外,当 M 不够用时,调度器会帮我们向操作系统申请新的系统级线程,而当某个 M 已无用时,调度器又会负责把它及时地销毁掉。

主goroutine

回到我们一开始的代码

func main() {
    for i := 0; i < 10; i++ {
        go func() {
            fmt.Println(i)
        }()
    }
}

与一个进程总会有一个主线程类似,每一个独立的 Go 程序在运行时也总会有一个主 goroutine。这个主 goroutine 会在 Go 程序的运行准备工作完成后被自动地启用,并不需要我们做任何手动的操作。
相信大家也看出来了,这个主 goroutine 的 go 函数(每条 go 语句都会携带一个函数调用,称为 go 函数)就是我们的 main 函数
一定要注意,go函数真正被执行的时间总会与其所属的go语句被执行的时间不同。当程序执行到一条go语句的时候,Go 语言的运行时系统,会先试图从某个存放空闲的 G 的队列中获取一个 G(也就是 goroutine),它只有在找不到空闲 G 的情况下才会去创建一个新的 G。拿到一个空闲的 G 之后,Go 语言运行时系统会用这个 G 去包装当前的那个go函数(或者说该函数中的那些代码),然后再把这个 G 追加到某个存放可运行的 G 的队列中。
队列中的G按照先入先出的顺序,由运行时系统内部的调度器安排运行,虽然很快,但还是会有耗时。
所以 go 函数的执行时间才会滞后于它所属 go 语句的执行时间。
go 语句本身执行完毕后,它不会等待 go 函数执行(异步并发),而是继续执行后面的语句。这就出现了上面的情况:主 goroutine 中的代码执行完毕,Go 程序就运行结束,程序都结束了,当然就没有那些子 goroutine,更谈不上执行了。

所以解决方案就是延迟主 goroutine 的执行结束时间,这样 gotoutine 就有机会执行了。上面用的是 time.Sleep,但还有更好的方案,使用 sync.WaitGroup,这里大家自己查找资料了解

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            fmt.Println(i)
            wg.Done()
        }()
    }
    wg.Wait()  
}

最终目的都是等待所有子 goroutine 执行完毕主 goroutine 才结束。

搞清楚了上面那些,现在看一下输出结果

lijun:class6/ $ go run class6_demo.go                                                             [18:33:03]
8
10
10
10
10
10
10
10
10
10

输出和预期的完全不一致。emmm~~~

根据上面的了解,goroutine 的执行是有滞后性的,但是这种滞后性是不确定的。
同时,调度器在分配 G 给每个 M 通过 P 执行的时候,虽然 G 在队列里按照先进先出的顺序拿出来,但是如何分配它是根据当前的各种资源调度情况来的,也就是说那个 G 先分配先执行完都不确定。除非人为干预(好像我也不知道怎么人为干预😂)

所以,滞后性让它的输出不可能是1,2,3,4...10,可能是5,8,10,10...10这样,同时不确定性又让每个 goroutine 执行的时机不一样,所以也有可能像这样

lijun:class6/ $ go run class6_demo.go                                                             [18:33:19]
5
10
5
10
10
10
10
8
10
10

而且由于上面的 go 函数中引用的是外部变量 i,可能情况就是:
1.当 go 函数所在语句执行结束后,goroutine 才执行,此时 i 变量已经是10了
2.当 go 函数所在语句还没有执行结束,goroutine 开始执行了,由于执行的时机不同,每个 go 函数获取的 i 变量可能是 5, 8,10这些。

如何让它正确输出

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(a int) {
            fmt.Println(a)
            wg.Done()
        }(i)
    }
    wg.Wait()  
}

通过给函数传值!
go 函数中,大多数参数都是值传递的,即会拷贝一份副本给函数,此时外界的变量和这个参数就没有任何关系了。这时候就能正确的打印。

lijun:class6/ $ go run class6_demo.go                                                             [18:35:52]
5
2
3
9
6
7
1
8
0
4

但是执行顺序还没有解决,这个问题留到后面解决吧!

以上是我通过一个 goroutine 的例子带来的思考以及学习过程,仅供大家思考!

来自 https://leejnull.github.io/2019/08/23/2019-08-23-01/

相关文章

  • 关于goroutine的一个简单案例的思考

    首先思考这样一个问题 下面这段代码会输出什么? 估计都不会说是 1,2,3,4...10的吧? 我们来看一下输出 ...

  • go

    https://golang.google.cn/ goroutine简单示例 goroutine将结果通过cha...

  • 关于goroutine

    goroutine是什么? 进程是“程序执行的一个实例” ,担当分配系统资源的实体。进程创建必须分配一个完整的独立...

  • 关于“简单”的思考

    今天读到成甲老师的一篇文章,文章前面提到:让自己过得简单。虽然我们缺乏足够的方法和勇气。 简单才能专注,专注才有力...

  • 关于简单的思考

    “简单,指不复杂;头绪少。也指草率;细致、平凡等。还有理解为单纯、不复杂,平凡、一般、普通等等......” ...

  • 一个关于弹窗案例的思考

    弹窗,顾名思义,是指弹出的窗口,强调一个弹字。无论是在web端还是在移动端,弹窗都是一种十分常见的一种交互方式,经...

  • 2017.8.11

    关于案例学习的思考 在案例学习中,我遇到的一个问题就是,有的案例是自己熟悉并曾经研究过的领域,但有部分案例是自己从...

  • golang技术笔记之channel(信道)

    信道是什么? 简单说,是goroutine之间互相通讯的东西。用来goroutine之间发消息和接收消息。 执行:...

  • 1.2.7go并发编程

    Go语言引入了goroutine概念,它使得并发编程变得非常简单。通过使用goroutine而不是裸用 操作系统的...

  • 关于golang的goroutine scheduler

    前言 G:指的是GoroutineM:工作线程或机器P:处理器,用来执行Go代码的资源每个M需要有一个关联P来执行...

网友评论

      本文标题:关于goroutine的一个简单案例的思考

      本文链接:https://www.haomeiwen.com/subject/screactx.html