Go 语言潜力有目共睹,但它的 Goroutine 机制底层原理你了解吗?

Go 语言潜力有目共睹,但它的 Goroutine 机制底层原理你了解吗?

来源 | 后端技术指南针(ID:gh_ed1e2b37dcb6)

Go 语言的巨大潜力有目共睹,今天我们来学习 Go 语言的 Goroutine 机制,这也可能是 Go 语言最为吸引人的特性了,理解它对于掌握 Go 语言大有裨益,话不多说开始吧!

通过本文你将了解到以下内容:

*什么是协程以及横向对比优势
*Go 语言的 Goroutine 机制底层原理和特点

Go 语言潜力有目共睹,但它的 Goroutine 机制底层原理你了解吗?

聊聊协程

大家对于进程、线程二位明星都很熟悉,但协程就没有火了,是协程不是携程哦!

协程并不是 Go 语言特有的机制,相反像 Lua、Ruby、Python、Kotlin、C/C++等也都有协程的支持,区别在于有的是从语言层面支持、有的通过插件类库支持。Go 语言是原生语言层面支持,本文也是从 Go 角度去理解协程。
Go 语言潜力有目共睹,但它的 Goroutine 机制底层原理你了解吗?

1.1 协程基本概念和提出者

协程英文是 Coroutine 译为协同程序,我们来看下维基百科对 Coroutine 的介绍:
Coroutines are computer program components that generalize subroutines for non-preemptive multitasking, by allowing execution to be suspended and resumed. Coroutines are well-suited for implementing familiar program components such as cooperative tasks, exceptions, event loops, iterators, infinite lists and pipes. According to Donald Knuth, Melvin Conway coined the term coroutine in 1958 when he applied it to construction of an assembly program.The first published explanation of the coroutine appeared later, in 1963. 简单翻译一下: 协同程序是一种计算机程序组件,它允许暂停和恢复执行,从而可以作为通用化的非抢占式多任务处理子程序。

协同程序非常适合实现例如协作任务、异常、事件循环、迭代器、管道等熟悉的程序组件。

根据唐纳德·克努特的说法,梅尔文·康威在 1958 年将 Coroutine 这个术语应用于装配程序的构建,直到在 1963 年才首次发表了阐述 Coroutine 的论文。协程的提出者梅尔文·爱德华·康威是一位计算机科学家,除了协程之外他还创造了 Conway's Law 康威定律,他基于社会学观察提出了系统设计的一些观点,本文就不展开了,感兴趣的可以看下作者的论文 How Do Committees Invent?:http://www.melconway.com/Home/Committees_Paper.html

1.2 协程和进线程的对比

我们来复习一下进线程和协程的一些基本特点吧 : 进程是系统资源分配的最小单位 , 进程包括文本段 text region、数据段 data region 和堆栈段 stack region 等。进程的创建和销毁都是系统资源级别的,因此是一种比较昂贵的操作,进程是抢占式调度其有三个状态 : 等待态、就绪态、运行态。进程之间是相互隔离的,它们各自拥有自己的系统资源 , 更加安全但是也存在进程间通信不便的问题。 进程是线程的载体容器,多个线程除了共享进程的资源还拥有自己的一少部分独立的资源,因此相比进程而言更加轻量,进程内的多个线程间的通信比进程容易,但是也同样带来了同步和互斥的问题和线程安全问题,尽管如此多线程编程仍然是当前服务端编程的主流,线程也是 CPU 调度的最小单位,多线程运行时就存在线程切换问题,其状态转移如图:
Go 语言潜力有目共睹,但它的 Goroutine 机制底层原理你了解吗?
协程在有的资料中称为微线程或者用户态轻量级线程,协程调度不需要内核参与而是完全由用户态程序来决定,因此协程对于系统而言是无感知的。协程由用户态控制就不存在抢占式调度那样强制的 CPU 控制权切换到其他进线程,多个协程进行协作式调度,协程自己主动把控制权转让出去之后,其他协程才能被执行到,这样就避免了系统切换开销提高了 CPU 的使用效率。
抢占式调度和协作式调度的简单对比:
Go 语言潜力有目共睹,但它的 Goroutine 机制底层原理你了解吗?
看到这里我们不免去想:看着协作式调度优点更多,那么为什么一直是抢占式调度占上风呢?让我们继续一起学习,可能就能解答这个问题了。

1.3 实际工作中的我们

我们写程序的时经常需要考虑的因素就是提高机器使用率,这个非常好理解。当然机器使用率和开发效率维护成本往往存在权衡,说句大白话就是:要么费人力要么费机器,选一个吧!

机器成本里面最贵的就是 CPU 了,程序一般分为 CPU 密集型和 IO 密集型,对于 CPU 密集型我们的优化空间可能没那么多,但对于 IO 密集型却有非常大的优化空间,试想我们的程序总是处于 IO 等待中让 CPU 呼呼睡大觉,那该多糟糕。

为了提高 IO 密集型程序的 CPU 使用率,我们尝试多进程 / 多线程编程等让多个任务一起跑分时复用抢占式调度,这样提高了 CPU 的利用率,但由于多个进线程存在调度切换,这也有一定的资源消耗,因此进线程数量不可能无限增大。

我们现在写的程序大部分都是同步 IO 的,效率还不够高,因此出现了一些异步 IO 框架,但是异步框架的编程难度比同步框架要大,但不可否认异步是一个很好的优化方向,先不要晕,来看下同步 IO 和异步 IO 就知道了: 同步是指应用程序发起 I/O 请求后需要等待或者轮询内核 I/O 操作完成后才能继续执行,异步是指应用程序发起 I/O 请求后仍继续执行,当内核 I/O 操作完成后会通知应用程序或者调用应用程序注册的回调函数。
我们以 C/C++开发的服务端程序为例,Linux 的异步 IO 出现的比较晚,因此像 epoll 之类的 IO 复用技术仍然有相当大的地盘,但是同步 IO 的效率毕竟不如异步 IO,因此当前的优化方向包括:异步 IO 框架 (像 boost.asio 框架) 和协程方案 (腾讯 libco)。
Go 语言潜力有目共睹,但它的 Goroutine 机制底层原理你了解吗?

Go 语言潜力有目共睹,但它的 Goroutine 机制底层原理你了解吗?

Go 和协程

我们知道协程是 Coroutine,Go 语言在语言层面对协程进行了原生支持并且称之为 Goroutine,这也是 Go 语言强大并发能力的重要支撑,Go 的 CSP 并发模型是通过 Goroutine 和 channel 来实现的,后续会专门写一下 CSP 并发模型。

2.1 协作式调度和调度器

协作式调度中用户态协程会主动让出 CPU 控制权来让其他协程使用,确实提高了 CPU 的使用率,但是不由得去思考用户态协程不够智能怎么办?不知道何时让出控制权也不知道何时恢复执行。
读到这里忽然明白了抢占式调度的优势了,在抢占式调度中都是由系统内核来完成的,用户态不需要参与,并且内核参与使得平台移植好,说到底还是各有千秋啊!
为了解决这个问题我们需要一个中间层来调度这些协程,这样才能让用户态的成千上万个协程稳定有序地跑起来,我们姑且把这个中间层称为用户态协程调度器吧!

2.2 Goroutine 和 Go 的调度器模型

Go 语言从 2007 年底开发直到今天已经发展了 12 年,Go 的调度器也不是一蹴而就的,在最初的几个版本中 Go 的调度器也非常简陋,无法支撑大并发。

经过多个版本的迭代和优化,目前已经有很优异的性能了,不过我们还是来回顾一下 Go 调度器的发展历程 (详见参考一):


Go 语言潜力有目共睹,但它的 Goroutine 机制底层原理你了解吗?
Go 的调度器非常复杂,篇幅所限本文只提一些基本的概念和原理,后续会深入去展开 Go 的调度器。

最近几个版本的 Go 调度器采用 GPM 模型,其中有几个概念先看下:


Go 语言潜力有目共睹,但它的 Goroutine 机制底层原理你了解吗?

GPM 模型使用一种 M:N 的调度器来调度任意数量的协程运行于任意数量的系统线程中,从而保证了上下文切换的速度并且利用多核,但是增加了调度器的复杂度。
来看两张图来进一步理解一下:


Go 语言潜力有目共睹,但它的 Goroutine 机制底层原理你了解吗?


Go 语言潜力有目共睹,但它的 Goroutine 机制底层原理你了解吗?

整个 GPM 调度的简单过程如下: 新创建的 Goroutine 会先存放在 Global 全局队列中,等待 Go 调度器进行调度,随后 Goroutine 被分配给其中的一个逻辑处理器 P,并放到这个逻辑处理器对应的 Local 本地运行队列中,最终等待被逻辑处理器 P 执行即可。 在 M 与 P 绑定后,M 会不断从 P 的 Local 队列中无锁地取出 G,并切换到 G 的堆栈执行,当 P 的 Local 队列中没有 G 时,再从 Global 队列中获取一个 G,当 Global 队列中也没有待运行的 G 时,则尝试从其它的 P 窃取部分 G 来执行相当于 P 之间的负载均衡。
Goroutine 在整个生存期也存在不同的状态切换,主要的有以下几种状态:

Go 语言潜力有目共睹,但它的 Goroutine 机制底层原理你了解吗?
画个状态图看下:


Go 语言潜力有目共睹,但它的 Goroutine 机制底层原理你了解吗?

Go 语言潜力有目共睹,但它的 Goroutine 机制底层原理你了解吗?

巨人的肩膀

*https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-goroutine/
*https://www.flysnow.org/2017/04/11/go-in-action-go-goroutine.html

*https://segmentfault.com/a/1190000018150987
*https://tiancaiamao.gitbooks.io/go-internals/content/zh/05.2.html
*https://wudaijun.com/2018/01/go-scheduler/
*https://zhuanlan.zhihu.com/p/77620605

【End】

Go 语言潜力有目共睹,但它的 Goroutine 机制底层原理你了解吗?

推荐阅读钉钉辟谣“老师能打开学生摄像头”;HTC 关闭官方社区;Node.js 安全版本发布 | 极客头条AI 医生“战疫”在前线2020 年 AI 如何走?Jeff Dean 和其他四位“大神”已做预测!远程办公众生相:“云”吃饭、被窝打卡、梳妆台编程 ......2020 年,云游戏将爆发?各大科技公司云游戏布局大曝光!SIM 卡交换攻击盗币猖獗,比特币从业者如何自保?Go 语言潜力有目共睹,但它的 Goroutine 机制底层原理你了解吗?你点的每一个在看,我认真当成了喜欢