协程(goroutine)

上一节我们介绍了并发与并行的区别,我们知道了Golang是一种并发编程语言。本节我们就来看下Golang是如何利用goroutine,即协程来实现并发的。

什么是goroutine(协程)

goroutine指的是并发执行的函数/方法,这里的函数/方法我们可以理解为任务。goroutine也可看作是轻量级的线程,创建goroutine的开销要远远小于线程,在Golang中创建成千上万个goroutine是常有的事情。

goroutine vs 线程

  • goroutine开销更小。goroutine仅占用几kb的内存开销,而且可以根据需要动态缩放,而线程占用的内存空间则更大,而且是固定不变的。

  • goroutine是在线程基础上的一层抽象封装,所有创建的的goroutine本质上是复用少量的若干个线程。比如可能只有一个线程,成千上万个goroutine都跑在这个线程上,分时复用。假设其中一个goroutine阻塞了该线程的执行,比如在等待用户输出,那么Golang运行时会帮我们再创建一个系统级线程,将所有其他goroutine切到新创建的线程上去。这些虽然听起来略显复杂,但是一般情况下我们无需关注这些底层的调度,Golang为我们提供好了方便易用的API,比如goroutine来实现并发编程。

  • goroutine通过channel,即管道来实现goroutine之间的通信。channel能帮我们解决共享内存冲突的问题,这也是相对于线程的一个优势。我们下一节会详细介绍channel

开启一个goroutine

开启一个goroutine的方式很简单,只需要在调用的函数/方法前加上关键字go即可。

下面我们创建一个goroutine

package main

import "fmt"

func hello() {
    fmt.Println("Hello world goroutine")
}
func main() {
    go hello()
    fmt.Println("main function")
}

这里go hello()即开启了一个新的goroutine,然后hello()这个任务和会和main()任务并发执行。main()函数是默认goroutine,我们一般称作主协程。多运行该程序几次你会发现有可能输出三种不同的结果,分别是:

main function
Hello world goroutine
main function
main function
Hello world goroutine

是不是有点惊讶?之所以会是这样的结果就是因为go hello()创建的协程和主协程是并发执行的,谁都有可能先执行完。这就好比两个人赛跑,谁先跑到终点都不一定。我们修改下前面的程序:

package main

import (
    "fmt"
    "time"
)

func hello() {
    fmt.Println("Hello world goroutine")
}
func main() {
    go hello()
    time.Sleep(1 * time.Second)
    fmt.Println("main function")
}

我们通过time.Sleep(1 * time.Second)来阻塞了主协程的执行,这时go hello()开启的协程便在主协程结束之前有足够多的时间去执行。该程序先输出Hello world goroutine,然后暂停了大约1秒钟,接着输出main function

这里我们使用time.Sleep虽然能保证程序执行结果的一致性,但是真实应用中并没有这么用的,这里仅仅是为了演示子协程和主协程的同步问题。正规的做法是使用channel,即管道。这个后面一节再详细介绍。

开启多个goroutine

下面我们再通过一个例子加深对goroutine的理解,这次我们来开启多个goroutine

package main

import (
    "fmt"
    "time"
)

func numbers() {
    for i := 1; i <= 5; i++ {
        time.Sleep(250 * time.Millisecond)
        fmt.Printf("%d ", i)
    }
}
func alphabets() {
    for i := 'a'; i <= 'e'; i++ {
        time.Sleep(400 * time.Millisecond)
        fmt.Printf("%c ", i)
    }
}
func main() {
    go numbers()
    go alphabets()
    time.Sleep(3000 * time.Millisecond)
    fmt.Println("main terminated")
}

该程序创建了两个子协程,这两个子协程并发执行。numbers协程每间隔250ms打印一个数字;alphabets协程每间隔400ms打印一个字母。主协程等待3000ms打印main terminated然后退出。该程序执行结果如下,而且是稳定输出:

1 a 2 3 b 4 c 5 d e main terminated

下图形象的说明了该程序的执行过程:

Last updated