# Go 快速入门(三)- goroutine

# 本章学习目标

go语言有着较高的开发效率,提供了海量并行的支持。听说过go语言的小伙伴应该都听说过这些特性。我们已经在前面章节体验到了使用go语言开发的便捷性。这一章节,我们来了解go语言并发利器goroutine

# 一、什么是goroutine

在了解goroutine之前,我们先简单了解一下进程与线程。

# 1、进程与线程

进程是资源分配的最小单位,线程是程序执行的最小单位。

进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种系统操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。

直白地讲,进程就是应用程序的启动实例。比如我们运行一个游戏,打开一个软件,就是开启了一个进程。进程拥有代码和打开的文件资源、数据资源、独立的内存空间。线程从属于进程,是程序的实际执行者。一个进程至少包含一个主线程,也可以有更多的子线程。

这儿有一篇能够让大家形象理解进程与线程关系的文章(进程与线程的一个简单解释),更多进程、线程知识大家自行了解啦。

# 2、goroutine就是协程?

我们先来看看什么是协程。

协程(coroutines)是一种协作任务控制机制,是一种比线程更加轻量级的存在。最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。协程的概念提出时间其实比线程还要早,不过是这几年才被大家熟知。

用生产者/消费者模式举例。创建一个消费者协程,并且在主线程中生产数据,协程中消费数据。当协程执行到需要生产数据地方时会暂停,等到主线程生产数据后,协程才会接到数据继续执行。但是,协程暂停和线程的阻塞是有本质区别的。协程的暂停完全由程序控制,线程的阻塞状态是由操作系统内核来进行切换。

本质上,goroutine 就是协程。 不同的是,go在 runtime、系统调用等多方面对goroutine调度进行了封装和处理,当遇到长时间执行或者进行系统调用时,会主动把当前 goroutine的CPU (P) 转让出去,让其他 goroutine 能被调度并执行,也就是 go 从语言层面支持了协程。

个人理解,goroutine实现了协程(coroutines)这一协作任务控制机制。

# 二、goroutine调度机制

要理解协程的实现,,首先需要了解go中的三个非常重要的概念,它们分别是GMP。这三项是协程最主要的组成部分, 它们在go的源代码中无处不在。

# 1、G (goroutine)

G是goroutine的首字母,goroutine可以解释为受管理的轻量线程,goroutine使用go关键词创建。存储了goroutine的执行stack信息、goroutine状态以及goroutine的任务函数等;

# 2、M (machine)

M是machine的首字母,基本等同于系统线程。M会从运行队列中取出G, 然后运行G, 如果G运行完毕或者进入休眠状态,则从运行队列中取出下一个G运行, 周而复始。

# 3、P (process)

P是process的首字母,代表逻辑处理器。即M运行G所需要的资源。P也可以理解为控制go代码的并行度的机制。P的数量决定了系统内最大可并行的G的数量(前提:系统的物理cpu核数>=P的数量)。

# 4、调度流程

当通过go关键字创建一个新的goroutine的时候,它会优先被放入P的本地队列。为了运行goroutine,M需要持有(绑定)一个P,接着M会启动一个OS线程,循环从P的本地队列里取出一个goroutine并执行。

上面介绍的goroutine调度流程只是简化版流程。work-stealing调度算法、goroutine的阻塞/唤醒、系统调用阻塞、大规模goroutine瓶颈等等问题都是goroutine调度过程中具体问题。欢迎感兴趣的小伙伴们一起来探讨。

# 三、goroutine使用

goroutine是Go并行设计的核心。goroutine说到底其实就是线程,但是它比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮我们实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存。也正因为如此,可同时运行成千上万个并发任务。goroutine比线程更易用、更高效、更轻便。

goroutine是通过go的runtime管理的一个线程管理器。goroutine通过go关键字实现了,其实就是一个普通的函数。

go hello(a, b, c)

通过关键字go就启动了一个goroutine。我们来看一个例子

package main

import (
    "fmt"
    "runtime"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        runtime.Gosched() // CPU把时间片让给别人,下次某个时候继续恢复执行该goroutine
        fmt.Println(s)
    }
}

func main() {
    go say("world") // 开一个新的Goroutines执行
    say("hello") // 当前Goroutines执行
}

// 以上程序执行后将输出:
// hello
// world
// hello
// world
// hello
// world
// hello
// world
// hello

我们可以看到go关键字很方便的就实现了并发编程。 上面的多个goroutine运行在同一个进程里面,共享内存数据,不过设计上我们要遵循:不通过共享来通信,而要通过通信来共享。后面章节我们会讲解如何通过通信来共享 。

# 本章试练