go语句及其执行规则

参考:https://time.geekbang.org/column/article/39841?utm_source=weibo&utm_medium=xuxiaoping&utm_campaign=promotion&utm_content=columns编程

不要经过共享数据来通信,偏偏相反,要以通信的方式共享数据。安全

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

一个进程至少会包含一个线程。若是一个进程只包含了一个线程,那么它里面的全部代码都只会被串行地执行。每一个进程的第一个线程都会随着该进程的启动而被建立,它们能够被称为其所属进程的主线程。异步

相对应的,若是一个进程中包含了多个线程,那么其中的代码就能够被并发地执行。除了进程的第一个线程以外,其余的线程都是由进程中已存在的线程建立出来的。ide

也就是说,主线程以外的其余线程都只能由代码显式地建立和销毁。这须要咱们在编写程序的时候进行手动控制,操做系统以及进程自己并不会帮咱们下达这样的指令,它们只会忠实地执行咱们的指令。不过,在 Go 程序当中,Go 语言的运行时(runtime)系统会帮助咱们自动地建立和销毁系统级的线程。这里的系统级线程指的就是咱们刚刚说过的操做系统提供的线程。函数

这带来了不少优点,好比,由于它们的建立和销毁并不用经过操做系统去作,因此速度会很快,又好比,因为不用等着操做系统去调度它们的运行,因此每每会很容易控制而且能够很灵活。ui

不过别担忧,Go 语言不但有着独特的并发编程模型,以及用户级线程 goroutine,还拥有强大的用于调度 goroutine、对接系统级线程的调度器。atom

这个调度器是 Go 语言运行时系统的重要组成部分,它主要负责统筹调配 Go 并发编程模型中的三个主要元素,
即:
G(goroutine 的缩写)
P(processor 的缩写)一种能够承载若干个 G,且可以使这些 G 适时地与 M 进行对接,并获得真正运行的中介
M(machine 的缩写) 系统级线程操作系统

从宏观上说,G 和 M 因为 P 的存在能够呈现出多对多的关系。当一个正在与某个 M 对接并运行着的 G,须要因某个事件(好比等待 I/O 或锁的解除)而暂停运行的时候,调度器总会及时地发现,并把这个 G 与那个 M 分离开,以释放计算资源供那些等待运行的 G 使用。线程

其中的 M 指代的就是系统级线程。而 P 指的是一种能够承载若干个 G,且可以使这些 G 适时地与 M 进行对接,并获得真正运行的中介。

而当一个 G 须要恢复运行的时候,调度器又会尽快地为它寻找空闲的计算资源(包括 M)并安排运行。另外,当 M 不够用时,调度器会帮咱们向操做系统申请新的系统级线程,而当某个 M 已无用时,调度器又会负责把它及时地销毁掉。

正由于调度器帮助咱们作了不少事,因此咱们的 Go 程序才老是能高效地利用操做系统和计算机资源。程序中的全部 goroutine 也都会被充分地调度,其中的代码也都会被并发地运行,即便这样的 goroutine 有数以十万计,也仍然能够如此。

go语句及其执行规则

什么是主 goroutine,它与咱们启用的其余 goroutine 有什么不一样?

与一个进程总会有一个主线程相似,每个独立的 Go 程序在运行时也总会有一个主 goroutine。这个主 goroutine 会在 Go 程序的运行准备工做完成后被自动地启用,并不须要咱们作任何手动的操做。

每条go语句通常都会携带一个函数调用,这个被调用的函数经常被称为go函数。而主 goroutine 的go函数就是那个做为程序入口的main函数。

go函数真正被执行的时间,总会与其所属的go语句被执行的时间不一样。当程序执行到一条go语句的时候,Go 语言的运行时系统,会先试图从某个存放空闲的 G 的队列中获取一个 G(也就是 goroutine),它只有在找不到空闲 G 的状况下才会去建立一个新的 G。

这也是为何我总会说“启用”一个 goroutine,而不说“建立”一个 goroutine 的缘由。已存在的 goroutine 老是会被优先复用

然而,建立 G 的成本也是很是低的。建立一个 G 并不会像新建一个进程或者一个系统级线程那样,必须经过操做系统的系统调用来完成,在 Go 语言的运行时系统内部就能够彻底作到了,更况且一个 G 仅至关于为须要并发执行代码片断服务的上下文环境而已。

在拿到了一个空闲的 G 以后,Go 语言运行时系统会用这个 G 去包装当前的那个go函数(或者说该函数中的那些代码),而后再把这个 G 追加到某个存放可运行的 G 的队列中

这类队列中的 G 老是会按照先入先出的顺序,很快地由运行时系统内部的调度器安排运行。虽然这会很快,可是因为上面所说的那些准备工做仍是不可避免的,因此耗时仍是存在的。

go函数的执行时间老是会明显滞后于它所属的go语句的执行时间。固然了,这里所说的“明显滞后”是对于计算机的 CPU 时钟和 Go 程序来讲的。咱们在大多数时候都不会有明显的感受。

在说明了原理以后,咱们再来看这种原理下的表象。请记住,只要go语句自己执行完毕,Go 程序彻底不会等待go函数的执行,它会马上去执行后边的语句。这就是所谓的异步并发地执行。

这里“后边的语句”指的通常是for语句中的下一个迭代。然而,当最后一个迭代运行的时候,这个“后边的语句”是不存在的。

在 demo38.go 中的那条for语句会以很快的速度执行完毕。当它执行完毕时,那 10 个包装了go函数的 goroutine 每每尚未得到运行的机会。

go函数中的那个对fmt.Println函数的调用是以for语句中的变量i做为参数的。你能够想象一下,若是当for语句执行完毕的时候,这些go函数都尚未执行,那么它们引用的变量i的值将会是什么?

它们都会是10,对吗?那么这道题的答案会是“打印出 10 个10”,是这样吗?

在肯定最终的答案以前,你还须要知道一个与主 goroutine 有关的重要特性,即:一旦主 goroutine 中的代码(也就是main函数中的那些代码)执行完毕,当前的 Go 程序就会结束运行。

若是在 Go 程序结束的那一刻,还有 goroutine 未获得运行机会,那么它们就真的没有运行机会了,它们中的代码也就不会被执行了。

当for语句的最后一个迭代运行的时候,其中的那条go语句便是最后一条语句。因此,在执行完这条go语句以后,主 goroutine 中的代码也就执行完了,Go 程序会当即结束运行。那么,若是这样的话,还会有任何内容被打印出来吗?

严谨地讲,Go 语言并不会去保证这些 goroutine 会以怎样的顺序运行。因为主 goroutine 会与咱们手动启用的其余 goroutine 一块儿接受调度,又由于调度器极可能会在 goroutine 中的代码只执行了一部分的时候暂停,以期全部的 goroutine 有更公平的运行机会。

因此哪一个 goroutine 先执行完、哪一个 goroutine 后执行完每每是不可预知的,除非咱们使用了某种 Go 语言提供的方式进行了人为干预。然而,在这段代码中,咱们并无进行任何人为干预。

那答案究竟是什么呢?就 demo38.go 中如此简单的代码而言,绝大多数状况都会是“不会有任何内容被打印出来”。

package main

import "fmt"

func main() {
    for i := 0; i < 10; i++ {
        go func() {
            fmt.Println(i)
        }()
    }
}
go run demo38.go //输出为空

怎样才能让主 goroutine 等待其余 goroutine?

一旦主 goroutine 中的代码执行完毕,当前的 Go 程序就会结束运行,不管其余的 goroutine 是否已经在运行了。那么,怎样才能作到等其余的 goroutine 运行完毕以后,再让主 goroutine 结束运行呢?

其实有不少办法能够作到这一点。其中,最简单粗暴的办法就是让主 goroutine“小睡”一下子。

既然不容易预估时间,那咱们就让其余的 goroutine 在运行完毕的时候告诉咱们好了。这个思路很好,但怎么作呢?

你是否想到了通道呢?咱们先建立一个通道,它的长度应该与咱们手动启用的 goroutine 的数量一致。在每一个手动启用的 goroutine 即将运行完毕的时候,咱们都要向该通道发送一个值。注意,这些发送表达式应该被放在它们的go函数体的最后面。对应的,咱们还须要在main函数的最后从通道接收元素值,接收的次数也应该与手动启用的 goroutine 的数量保持一致。关于这些你能够到 demo39.go 文件中,去查看具体的写法。

其中有一个细节你须要注意。我在声明通道sign的时候是以chan struct{}做为其类型的。其中的类型字面量struct{}有些相似于空接口类型interface{},它表明了既不包含任何字段也不拥有任何方法的空结构体类型。

注意,struct{}类型值的表示法只有一个,即:struct{}{}。而且,它占用的内存空间是0字节。确切地说,这个值在整个 Go 程序中永远都只会存在一份。虽然咱们能够无数次地使用这个值字面量,可是用到的却都是同一个值。

再说回当下的问题,有没有比使用通道更好的方法?若是你知道标准库中的代码包sync的话,那么可能会想到sync.WaitGroup类型。没错,这是一个更好的答案

package main

import (
    "fmt"
    //"time"
)

func main() {
    num := 10
    sign := make(chan struct{}, num)

    for i := 0; i < num; i++ {
        go func() {
            fmt.Println(i)
            sign <- struct{}{}
        }()
    }

    // 办法1。
    //time.Sleep(time.Millisecond * 500)

    // 办法2。
    for j := 0; j < num; j++ {
        <-sign
    }
}
go run demo39.go 
10
10
10
10
10
10
10
10
6
10

怎样让咱们启用的多个 goroutine 按照既定的顺序运行?

go函数中先声明了一个匿名的函数,并把它赋给了变量fn。这个匿名函数作的事情很简单,只是调用fmt.Println函数以打印go函数的参数i的值

在这以后,我调用了一个名叫trigger的函数,并把go函数的参数i和刚刚声明的变量fn做为参数传给了它。注意,for语句声明的局部变量i和go函数的参数i的类型都变了,都由int变为了uint32。至于为何,我一下子再说。

再来讲trigger函数。该函数接受两个参数,一个是uint32类型的参数i, 另外一个是func()类型的参数fn。你应该记得,func()表明的是既无参数声明也无结果声明的函数类型。

trigger函数会不断地获取一个名叫count的变量的值,并判断该值是否与参数i的值相同。若是相同,那么就当即调用fn表明的函数,而后把count变量的值加1,最后显式地退出当前的循环。不然,咱们就先让当前的 goroutine“睡眠”一个纳秒再进入下一个迭代

我操做变量count的时候使用的都是原子操做。这是因为trigger函数会被多个 goroutine 并发地调用,因此它用到的非本地变量count,就被多个用户级线程共用了。所以,对它的操做就产生了竞态条件(race condition),破坏了程序的并发安全性。

老是应该对这样的操做加以保护,在sync/atomic包中声明了不少用于原子操做的函数。

因为我选用的原子操做函数对被操做的数值的类型有约束,因此我才对count以及相关的变量和参数的类型进行了统一的变动(由int变为了uint32)。

纵观count变量、trigger函数以及改造后的for语句和go函数,我要作的是,让count变量成为一个信号,它的值老是下一个能够调用打印函数的go函数的序号。

这个序号其实就是启用 goroutine 时,那个当次迭代的序号。也正由于如此,go函数实际的执行顺序才会与go语句的执行顺序彻底一致。此外,这里的trigger函数实现了一种自旋(spinning)。除非发现条件已知足,不然它会不断地进行检查。

最后要说的是,由于我依然想让主 goroutine 最后一个运行完毕,因此还须要加一行代码。不过既然有了trigger函数,我就没有再使用通道。

调用trigger函数彻底能够达到相同的效果。因为当全部我手动启用的 goroutine 都运行完毕以后,count的值必定会是10,因此我就把10做为了第一个参数值。又因为我并不想打印这个10,因此我把一个什么都不作的函数做为了第二个参数值。
总之,经过上述的改造,我使得异步发起的go函数获得了同步地(或者说按照既定顺序地)执行,你也能够动手本身试一试,感觉一下。

package main

import (
    "fmt"
    "sync/atomic"
    "time"
)

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
            }
            time.Sleep(time.Nanosecond)
        }
    }
    for i := uint32(0); i < 10; i++ { //咱们传给go函数的参数i会先被求值,如此就获得了当次迭代的序号。以后,不管go函数会在何时执行,这个参数值都不会变
        go func(i uint32) {
            fn := func() {
                fmt.Println(i)
            }
            trigger(i, fn)
        }(i)
    }
    trigger(10, func() {})
}
go run demo40.go 
0
1
2
3
4
5
6
7
8
9
相关文章
相关标签/搜索