每当我看着大海的时候,我总想找人谈谈。但当我和人交谈时,我又总想去看看大海。 —— 村上春树
go在语言层面对协程进行了原生的支持并且称为goroutine,这也是go语言强大并发能力的重要支撑。
本文并非一蹴而就,而是蓄势待发已久。后续随着知识及理解的不断深入仍将持续补充进来。同时也欢迎各位留言探讨,互相学习。
目录
进程、线程、协程
1,进程是系统资源分配的最小单位,线程是CPU调度的最小单位;
2,进程是线程的载体,一个进程可由多个线程组成,进程内的多个线程间可以相互通信;
3,进程的创建和销毁都是系统资源级别,因此是一种比较昂贵的操作;
4,线程是抢占式调度,协程是协作式调度,多线程无可避免的会带来频繁的CPU上下文切换,调度成本高,但抢占式调度由系统内核来完成的,用户态不需要参与,内核参与使得平台移植好;协作式调度中用户态协程会主动让出CPU控制权来让其他协程使用;
5,协程不和内核交互,协程是用户态轻量级线程;
6,线程与协程可简单的理解为捆绑关系,任意数量的用户态协程可以运行在任意数量的os线程上;用户创建的协程会被专门负责管理goroutine的processor接管调度,一个processor可对应N个协程;
7,原生的linux线程从进程栈分配空间,所分配的线程栈空间大小默认为8M:
而创建一个协程分配的空间是2k,空间可能会自动扩增。
为什么会选择协程
原生线程的调度成本高,那么go语言自己进行了接管调度---go runtime进行调度。
goroutine调度模型-GPM
源码位置:src/runtime/proc.go
// Goroutine scheduler
// The scheduler's job is to distribute ready-to-run goroutines over worker threads.
//
// The main concepts are:
// G - goroutine.
// M - worker thread, or machine.
// P - processor, a resource that is required to execute Go code.
// M must have an associated P to execute Go code, however it can be
// blocked or in a syscall w/o an associated P.
G:goroutine
M:系统内核线程
P:执行go代码所需的处理器,是负责golang运行时的线程,负责执行goroutine
M必须具有关联的P才能调度goroutine,即每个P对应着一个M。新创建的goroutine会先存放在待调度队列(图中灰色部分的G所在队列)中,等待调度器进行调度。
M与P绑定后,M先从P的本地运行队列(图中浅蓝色部分的G所在的待运行队列)中取出G,并切换到G的堆栈执行,当P的本地队列中没有G时,再从待调度队列中获取一个G分配给这个P,当待调度队列中也没有待运行的G时,则尝试从其它的P窃取部分G来执行。
通过这种模型,go避免了大量的上下文切换。
并发与并行
文末,顺带聊聊并发与并行
并行:你在吃饭,来了个电话,无关紧要,你一边吃一边说;
并发:你在吃饭,来了个电话,因为一些因素必须通完电话才能继续吃饭(你的身体先由打电话支配使用),通完电话后继续吃饭(身体切换到可以吃饭)。