前言
最近我把Go重新过了一遍,特别是Go的协程这一块,我感觉任何事都是从简单到复杂
包括现在,所以我重新开始学习基础,后序我会出一个系列
分别为
- Golang协程基础 (已经完成!)
- Golang协程调度
- Golang协程控制
- Golang协程通信
- Golang垃圾回收机制
所以我会持续更新,大家请期待吧,爱你们!
Go里的协程是什么
写Go这么长时间了,在开发项目当中,感觉Golang的好处还是很多的
Golang为什么被推崇,核心就是在并发和协程方面有很大的优势
协程这个概念其实不陌生,我在大学看Python的时候就看过这方面的资料
就是轻量级的线程
但是Go的协程其实和Python又不太一样了,这里我还是认真讲一下协程是个什么玩意儿吧
进程和线程
协程是轻量级的线程,但是线程又和进程有不能说的PY交易
所以我首先来过一遍进程和线程的基础概念
首先直接说概念,这个是核心,记住就完事
- 线程是进程的组成部分
- 一个线程只能有一个进程
- 但是一个进程可以有多个线程
- 进程死了,线程一起死
- 进程之间相互独立
- 进程开销比线程大
可以这么理解,线程是进程的崽,进程是独立个体,可以随意下崽(创建线程)
操作系统里面,调度到CPU中执行的最小单位就是线程
有多核处理器的计算机,线程可以分布在多个CPU上,实现真正的并行逻辑。
看图,这就是多核处理器和单核处理器的差别(Python就是上面那个。。。GIL让他永远用不了多核
线程的切换和调度
这里还是分析线程,是为后序分析协程逻辑打好基础,要是大家伙不想看,直接跳过即可
线程好用,但是不能一下开无数个,也不能全开,也不能不返回就持续等待
所以,为了最大化利用CPU资源,操作系统需要通过定时器,IO设备,上下文切换等动作去控制线程
一般的切换逻辑是
当A线程发生切换的时候,会从用户态转移到内核中,记录寄存器值,进程状态之类的信息到操作系统线程的控制块当中
然后进行切换,切换到下一个执行线程B,加载刚刚保存那得那些寄存器值,然后从内核转移到用户态中
如果线程A和线程B不属于同一个线程,切换的时候将会更新额外的状态信息和内存地址,然后导入页表到内存里面。
可以看见一点,就是线程切换需要记录一大堆东西
这就是线程切换的一点点知识。
线程和协程的关系
协程,轻量级线程,相当于线程PLUS
但是它和线程不同的一点,人家切换或者是做别的操作
不依赖操作系统内核,而依赖自身(Go)的调度器
其实就是内部执行的一串代码
协程其实是线程的从属
下面分析下他们的具体区别还有联系
GMP模型-线程和协程的核心关系
首先看个图
这个图就是GMP模型的核心图,这个图其实说的挺明白了
首先
G就是协程
P就是Go的调度器
M就是线程
可以看见,Go的协程依托线程
一个P可能包含了多个协程,一个P在任何时候只能有一个M
这就说明了线程和协程的关系应该是 m:n
相关的知识我也在其他的文章里说过
但是我后续还是会细化一下GMP模型
协程的调度方式
首先看个图
协程和线程的关系为M:N,为多对多关系
调度器P可以将多个协程调度到一个线程中,也可以切换一个协程到多个线程中运行
协程的切换
协程为什么叫轻量级线程,因为它的切换速度比线程快,根据上面我讲的线程切换
线程切换需要操作系统用户态和内核态,并且需要存储寄存器的变量值,保留额外的一些变量值(上面有说
协程切换只需要保留极少的状态值和寄存器变量值,并且一切都有Go调度器去操作,免去用户态和操作系统态交互的麻烦逻辑
线程切换的速度大概是 1-2微秒
协程切换的速度为 0.2 微秒
协程的调度策略
这里后面我的Blog也会讲,在这我就简单描述一下吧
线程的调度大部分都是抢占式的,为了平衡资源,操作系统的调度器会定时发出中断信号,来进行线程切换
协程的调度是协作式的,当一个协程处理完任务的时候,可以将执行权限交还给其他协程,不会被轻易抢占,并且协程切换也是有一定的方法,比如抢占队列,偷窃任务等等,这个我后面会另开一篇好好讲述
协程栈的大小
线程栈大小,一般是创建时指定的,为了避免溢出,默认一个栈会比较大(2MB)
这样就大大限制了线程栈的数量,如果1000个线程就需要占用2GB虚拟内存
但是协程栈大小默认为2KB,这样就可以创建一大堆协程,并且协程可以动态扩容栈大小
而线程只能固定一个栈大小
协程栈扩容的算法我后面会说(自己老开坑。。。
并发和并行
并发,并行是老生常态的话题了
我这简单说下
并发:谁先执行任务我不管,但是,某个时间段内,所有的任务都能执行完毕
并行:一起执行任务,大家一起出发
在我开发的生涯里面,这种情况不只是单纯的并发并行,一般都是一起用的
除开Python(有GIL锁)
基本都是多核并行处理多个线程任务,但是单核里面也在负责多个线程任务
所以这样的关系类似
转回Go部分,Go的调度器,会把协程分给多个线程,这些线程又很可能被分发给了不同的进程,这样的关系就类似
这样也说明,在多核的环境下,Go的并发,并行是同时存在且不冲突的。
写一个协程实例
这个我相信大家玩Go的人都会写,其实Go的协程非常简单
1 | go test() |
那么现在咱们要实现一个并发协程,首先实现一个线性流程
1 | package main |
这边输出
1 | yemilice |
现在我们改写成协程模式
1 | package main |
得到输出
1 | day |
这就是一个简单的协程,这部分我用了time.sleep做通信等待,这个比较low,我后续会在协程通信的blog里面更详细的描述介绍
例如channel,waitgroup等
Go的并发模型
上一节我们实现了一个Go的协程实例,但是似乎我们并不了解为什么Go要这么设计
这一节我将告诉你什么是Go的并发模型,也就是Go执行的流程,这一节也是为下次的文章做好基础,比较重要
其实Go遵循的是fork-join的一种并发模型
fork可以指程序中任何地方,这里将子协程和主协程分开执行
join指的是某个时候,子协程和主协程执行分支合并在一起
看个图
这就是fork-join的并发模型
举个简单的例子
1 |
|
这里我们去执行,但是什么都没有返回
这里如果用fork-join表示,画图如下
可见我们需要连接点
将代码改写
1 | func work() { |
这里有了连接点,用fork-join表示一下就是
这里的代码不用深究,这里属于协程控制,这里后面我会单开博客讲
这里只是为了让大家直观看到fork-join这种并发模型,这是Go预言需要遵循的并发哲学
总结
这次主要是把协程的基础概念讲了一下
- 讲了协程和线程的关系
- 讲了协程是怎么来的,怎么实现一个协程
- 讲了Go的并发模型
下集预告
下一节我将深入到Go协程之间的调度当中
- 整明白Go协程的调度原理
- 整明白Go协程的调度策略
- 整明白Go协程的几种状态