# 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中的三个非常重要的概念,它们分别是G、 M和P。这三项是协程最主要的组成部分, 它们在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
运行在同一个进程里面,共享内存数据,不过设计上我们要遵循:不通过共享来通信,而要通过通信来共享。后面章节我们会讲解如何通过通信来共享 。