这是我参与「第五届青训营」伴学笔记创作活动的第 2 天
对协程的理解
协程:用户态线程
协程:goroutine
协程定义
协程的英文名并不是goroutine,而是coroutine。
协程是计算机程序的一类组件,推广了协作式多任务的子例程,允许执行被挂起与被恢复。
通俗来讲:
协程是可暂停可恢复的执行函数
协程(coroutine)与线程(thread)的区别
- 两者从本质上讲就有不同之处:线程是由操作系统控制,而协程是由用户自己控制的,所以二者从控制层面就有了根本性的区别,网传的“协程就是用户态线程”的概念,也是为了便于理解而产生的名词。
- 线程thread始终是一个系统化的概念名词,它的核心在于
调度,而coroutine实际上是一个编程语言级别的概念,其实更应该理解为协函数,只是它的设计思想与系统的线程思想有些类似,所以在命名时加上“程”字结尾。
Go语言在协程开发(CSP模型)上做了什么?
-
Channel机制:多个goroutine在并行过程中,通信的方式是Channel,数据通过一根管道传输到另一个协程中进行处理。这种设计理念来自于
Communicating Sequential Processes,也就是后人口中的CSP模型。早在1984年就被提出解决并发问题。不要以共享内存的方式来通信,相反,要通过通信来共享内存。
笔者绘制了一幅图,希望有助于各位理解两种不同的通信方式的区别。对于三个并发执行的协程,由于在时间上具有同时性,如果要对同一块内存进行资源访问,必然会引起争抢问题。针对这个问题有不同的解决方案,“加锁”便是其中的解决方法之一。但是加锁在开发过程中难免引起许多问题,造成生产事故/开发效率低下,Go语言倡导的就是通过channel进行协程之间的通信,如右图所示,一个变量的数据被goroutine 1打入channel,而channel在另外两个协程进行输出。
-
WaitGroup操作:熟悉Go语言开发的朋友都知道,WaitGroup是一个用来阻塞主协程/更大协程的工具。
var wg sync.WaitGroup wg.Add(5) for i:=0;i<5;i++{ go func(){ defer wg.Done() //do something... } } wg.Wait()通过指定好数目
wg.Add(number int)来指定要等待完成的数目,这里的for循环表示开启五个协程,依次wg.Done()以减少挂起的协程数目。最外面的wg.Wait()用来阻塞主协程(Main coroutine),达到实现目标的目的。
协程与生产开发
实际案例引入
如果现在有一项业务,当只有10个人访问的时候,我们可以开10个线程,同时去查询数据库;当有100个人访问的时候,开启100条线程,好像也没什么问题。但是遇到巨大访问量的时候,多开线程的方法恐怕不现实。
每一个线程会占用至少4M的存储空间,如果遇到双十一这样巨大的流量情况并且坚持以多开线程的方式进行处理,恐怕带来的内存开销是服务器无法招架的。
操作系统在线程等待IO的时候,会阻塞当前线程,切换到其它线程。也就是说如果一个线程突然没了访问动静,系统会将线程进行切换,这样在当前线程等待IO的过程中,其它线程可以继续执行。但是过多的线程切换操作会占用大量的系统时间。
在编码层面,我们可以用协程解决这个问题,处理到某样逻辑的时候,自动切换到另一个协程(程序逻辑),协程的切换开销成本比线程的切换开销成本小得多,这样通过人为的方式减少了内存开销。
但是协程的主要作用并不在这里,而是解决IO问题。
(核心)协程与IO
IO其实就是初学者学习过程中常用的“从键盘读取,从控制台输出”,这就是最简单的IO,互联网后端其实有一大核心问题就是IO,因为机器是不知道什么时候会有巨额访问量的,这就是IO的随机性、不可预期性。
多线程/线程池的IO解决方案,可能会导致Data Race,并且也有开销大等问题(上一章节提到),现流行的解决方案之一就是异步框架(事件驱动LibEvent),但是异步框架的开发过程不符合人类的认知习惯,确实使开发流畅性降低,编码难度较高。Go语言选用CSP作为解决方案,这种符合人类认知的设计使得开发难度大大降低。
基于以上讨论,协程与异步IO的结合,才能最大限度发挥协程的作用。此处限于篇幅不再赘述,读者可自行查找相关资料。