go语句及其执行规则

学习之前先看一下下面这句话:

Don’t communicate by sharing memory; share memory by communicating.
不要通过共享数据来通讯,要以通讯的方式共享数据。

通道(也就是 channel)类型的值可以被用来以通讯的方式共享数据。更具体地说,它一般被用来在不同的goroutine之间传递数据。
这篇主要讲goroutine是什么。简单来说,goroutine代表着并发编程模型中的用户级线程。

调度器

Go语言不但有着独特的并发编程模型,以及用户级线程goroutine,还拥有强大的用于调度goroutine、对接系统级线程的调度器。
这个调度器是Go语言运行时系统的重要组成部分,它主要负责统筹调配Go并发编程模型中的三个主要元素:

  • G(goroutine 的缩写),用户级线程
  • P(processor 的缩写),一种可以承载若干个G,且能够使用这些G适时的与M进行对接,并得到真正运行的中介
  • M(machine 的缩写),系统级线程

主goroutine

这里需要知道一个与主goroutine有关的重要特性,一旦主goroutine中的代码(也就是main函数中的那些代码)执行完毕,当前的 Go 程序就会结束运行。
先看下面这个例子:

package main

import "fmt"

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

上面的程序运行之后,不会有打印任何内容。
只要go语句本身执行完毕,Go程序完全不会等待go函数的执行,它会立刻去执行后面的语句。这就是所谓的异步并发地执行。
在上面的例子中,在for语句执行完毕后,里面包装的10个goroutine还没有获得运行的机会,主goroutine中的代码执行完了,Go程序就会立即结束运行。

使用Sleep等待

上面的例子中,如果要让程序在其他goroutine运行完之后再退出。最简单粗暴的办法是Sleep一段时间:

package main

import (
    "fmt"
    "time"
)

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

这个办法可行,但是Sleep的时间需要预估。太长会浪费时间,太短则不能保证所有goroutine都运行完毕。不容易预估时间,最好是让其他的goroutine在运行完毕后发送通知。

让主goroutine等待其他goroutine

使用通道,通道的长度与启用的goroutine的数量一致。每个goroutine运行完毕前,都向通道发送一个值。在主goroutine则是从这个通道接收值,接收了足够数量的次数后就说明所有goroutine都运行完毕了,可以继续往下执行了(就是退出):

package main

import "fmt"

func main() {
    sign := make(chan struct{}, 10)
    for i := 0; i < 10; i++ {
        go func() {
            fmt.Println(i)
            sign <- struct{}{}
        }()
    }
    for j := 0; j < 10; j++ {
        <- sign
    }
}

这里声明的通道的类型是 chan struct{} ,是一个空结构体。它谭勇的内存空间是0字节。这个值在整个Go程序中永远都只会存在一份。虽然可以无数次的使用这个值字面量,但是用到的都是同一个值。当把通道仅仅刀座是传递某个简单信号的介质的时候,使用空结构体是最好的。
其他方式
在标准库中,有一个sync包,里面有一个sync.WaitGroup类型。这应该是一个更好的实现方式。不过这要等后面讲sync包的时候再说了。

让多个goroutine按照既定的顺序执行

首先改造一下一只使用的例子,把变量i的值传递给每个goroutine,这样输出的是0-9各一次,不过是乱序的:

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

讲师的例子

package main

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

var count uint32

func trigger (i uint32, fn func()) {
    for {
        if n := atomic.LoadUint32(&count); n == i {
            fn()
            atomic.AddUint32(&count, 1)
            break
        }
        time.Sleep(time.Nanosecond)
    }
}

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

主要就是trigger函数。在trigger里会检查i,并把要执行的语句打包成fn函数也传入,只有在trigger里判断后符合条件,就会执行fn函数的语句。
trigger里会检查i和count是否相等,在执行了fn函数后,需要把count加1,这里用了原子操作。里有是trigger函数会被多个goroutine并发的调用,所以这个变量被多个用户级线程共用了。因此对它的操作就产生了竞态条件(race condition),破坏了程序的并发安全性。
在最后退出的时候,应该有了trigger函数,只要检查count是否到10了,就表示其他goroutine都执行完了,所以也就不需要通道了。
另外在trigger函数里,是一个for语句的无限循环,在判断条件不成立后,先进行了一个1纳秒的Sleep。如果不加这句的话,测试下来,偶尔会出现程序卡住的情况(甚至是死机)。这里加上Sleep语句应该是希望这个时候程序可以进行一下切换,否则当前应该执行的那个goroutine如果拿不到执行的机会,其他goroutine也都无法通过if条件的判断。

自己的实现

package main

import (
    "fmt"
    "time"
)

func main() {
    sign := make(chan struct{}, 10)
    var count int
    for i := 0; i < 10; i++ {
        go func(i int) {
            for {
                if count == i{
                    fmt.Println(i)
                    count ++
                    sign <- struct{}{}
                    break
                }
                time.Sleep(time.Nanosecond)
            }
        }(i)
    }
    for j := 0; j < 10; j++ {
        <- sign
    }
}

主要两个问题,当时没有意识到在for无限循环之后,进入下一个迭代前,这个1纳秒Sleep的意义。还有就是我没有使用原子操作。不过这里即使不用原子操作也没问题的样子,因为逻辑上通知只有一个goroutine满足条件会去操作共用的变量count。所以这里和上面讲师的示例就差在对变量count的比较和判断是否是原子操作的问题上了。

原子操作
这里再自我做一些补充。

原子操作,即执行过程不能被中断的操作(并发)。
经典问题:i++是不是原子操作?
答案是否,因为i++看上去只有一行,但是背后包括了多个操作:取值,加法,赋值。