1 梳理概念:进程、线程、协程
1.1 进程
在《计算机操作系统》一书中,进程这样被解释:
进程是进程实体的运行过程,是程序的基本执行实体,是系统进行资源分配和调度的一个独立单位。
进程实体 = 程序段 + 相关数据段 + 进程控制块(PCB),
进程的特性:①动态性 ②并发性 ③独立性 ④异步性 ⑤结构性
进程的三种基本状态:
就绪(Ready)状态、执行(Running)状态、阻塞(Block)状态
1.2 线程
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。
一个进程可以有很多线程,每条线程并行执行不同的任务。
线程的基本状态:派生,阻塞,激活,调度,结束
1.3 协程
协程,又称微线程,纤程。英文名Coroutine。一句话说明什么是协程:协程是一种用户态的轻量级线程。
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。
在并发编程中,协程与线程类似,每个协程表示一个执行单元,有自己的本地数据,与其它协程共享全局数据和其它资源。
协程的基本状态:见下文。
2 为什么协程能更好支持并发
因为协程是用户自己来编写调度逻辑的,对CPU来说,协程其实是单线程,所以CPU不用去考虑怎么调度、切换上下文,这就省去了CPU的切换开销,所以协程在一定程度上又好于多线程。
对比Java的线程:
内核线程 | 用户线程 | |
Java线程 | 1 | 1 |
Go协程 | m | n |
因此协程的好处:
- 无需线程上下文切换的开销
- 无需原子操作锁定及同步的开销
- 方便切换控制流,简化编程模型
- 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。
3 Go项目中goruntine的使用方式
3.1 简单使用
package main func main() { go func() { println("Hello goruntine") }() } 复制代码
为什么没有输出协程的输出语句,是因为主线程先执行完了,下面我们来试下:
package main import "time" func main() { go func() { println("Hello goruntine") }() time.Sleep(3 * time.Second) } 复制代码
3.2 控制使用
控制协程的数量
(1)使用sync.WaitGroup控制协程数量
package main import ( "fmt" "sync" "time" ) func main() { var wg sync.WaitGroup fmt.Println("This is main thread start ...") wg.Add(2) go func() { defer wg.Done() fmt.Println("Go 1 finished ...") }() go func() { defer wg.Done() time.Sleep(3 * time.Second) fmt.Println("Go 2 finished ...") }() wg.Wait() fmt.Println("This is main thread finished ...") } 复制代码
(2)使用sync.Mutex控制协程数量
package main import ( "fmt" "sync" ) func main() { var locker = new(sync.Mutex) var cond = sync.NewCond(locker) var done bool = false fmt.Println("Main is start...") cond.L.Lock() go func() { fmt.Println("Go is finished... ") done = true cond.Signal() }() fmt.Println("Main is wait...") if !done { cond.Wait() cond.L.Unlock() } fmt.Println("Main is finished...") } 复制代码
4 goruntine的基本原理
4.1 源码
package runtime中的runtime2.go文件中:
//Stack描述了一个Go执行堆栈。堆栈的边界是[lo, hi),两边都没有隐式数据结构。 type stack struct { lo uintptr //下界 hi uintptr //上界 } type g struct { // Stack信息. stack stack // offset known to runtime/cgo stackguard0 uintptr // offset known to liblink stackguard1 uintptr // offset known to liblink _panic *_panic // innermost panic - offset known to liblink _defer *_defer // innermost defer m *m // current m; offset known to arm liblink sched gobuf // goroutine切换时,用于保存g的上下文 ...... } 复制代码
4.2 原理简述
通过go关键字调用底层函数runtime.newproc()创建一个goroutine
当调用该函数之后,goroutine会被设置成runnable状态
goroutine 本身只是一个数据结构,真正让 goroutine 运行起来的是调度器。Go 实现了一个用户态的调度器(GMP模型),这个调度器充分利用现代计算机的多核特性,同时让多个 goroutine 运行,同时 goroutine 设计的很轻量级,调度和上下文切换的代价都比较小。
4.3 goruntine状态
const ( // G status _Gidle = iota //空闲中。G刚刚新建, 仍未初始化 _Grunnable //待运行。就绪状态,G在运行队列中, 等待M取出并运行 _Grunning //运行中。M正在运行这个G, 这时候M会拥有一个P _Gsyscall //系统调用中。M正在运行这个G发起的系统调用, 这时候M并不拥有P _Gwaiting // 等待中。G在等待某些条件完成, 这时候G不在运行也不在运行队列中(可能在channel的等待队列中) _Gmoribund_unused // 当前未使用。但在gdb中硬编码脚本。 _Gdead // 已中止。G未被使用, 可能已执行完毕 _Genqueue_unused // 当前未使用。 _Gcopystack // 栈复制中。G正在获取一个新的栈空间并把原来的内容复制过去(用于防止GC扫描) _Gpreempted // 停止自己来暂停抢占。 它类似于_Gwaiting,并且不在运行队列上,等待唤醒。 // 以下状态为GC相关 _Gscan = 0x1000 _Gscanrunnable = _Gscan + _Grunnable // 0x1001 _Gscanrunning = _Gscan + _Grunning // 0x1002 _Gscansyscall = _Gscan + _Gsyscall // 0x1003 _Gscanwaiting = _Gscan + _Gwaiting // 0x1004 _Gscanpreempted = _Gscan + _Gpreempted // 0x1009 ) 复制代码
(图片链接:inews.gtimg.com/newsapp_bt/…
5 Go的重要概念:GMP模型
- M代表一个内核线程,也可以称为一个工作线程。goroutine就是跑在M之上的。
- P代表着处理器(processor) ,它的主要用途就是用来执行goroutine的,一个P代表执行一个go代码片段的基础(上下文环境),所以它也维护了一个可运行的goroutine队列,和自由的goroutine队列,里面存储了所有需要它来执行的goroutine。
- G 代表着goroutine 实际的数据结构(就是你封装的那个方法) ,并维护者goroutine 需要的栈、程序计数器以及它所在的M等信息。
- Seched代表着一个调度器,它维护有存储空闲的M队列和空闲的P队列,可运行的G队列,自由的G队列以及调度器的一些状态信息等。
5.1 用户协程(goroutine )
代码实现的协程,包含函数执行的指令和参数,任务对象,线程上下文切换,字段保护,和字段的寄存器。
type g struct { stack stack // offset known to runtime/cgo stackguard0 uintptr // offset known to liblink stackguard1 uintptr // offset known to liblink _panic *_panic // innermost panic - offset known to liblink _defer *_defer // innermost defer m *m // current m; offset known to arm liblink sched gobuf ...... } 复制代码
5.2 内核线程(machine)
M是一个线程,每个M都有一个线程的栈。如果没有给线程的栈分配内存,操作系统会给线程的栈分配默认的内存。当线程的栈制定,M.stack->G.stack, M的PC寄存器会执行G提供的函数。
type m struct { g0 *g // goroutine with scheduling stack morebuf gobuf // gobuf arg to morestack divmod uint32 // div/mod denominator for arm - known to liblink procid uint64 // for debuggers, but offset not hard-coded gsignal *g // signal-handling g goSigStack gsignalStack // Go-allocated signal handling stack sigmask sigset // storage for saved signal mask tls [6]uintptr // thread-local storage (for x86 extern register) mstartfn func() curg *g // current running goroutine caughtsig guintptr // goroutine running during fatal signal p puintptr // attached p for executing go code (nil if not executing go code) nextp puintptr oldp puintptr // the p that was attached before executing a syscall id int64 mallocing int32 throwing int32 preemptoff string // if != "", keep curg running on this m ...... } 复制代码
5.3 处理器(processor)
prosessor处理器是用来处理goroutine的,processor包含着每一个goroutine的资源。
一个Processor有很多goruntine组成的等待队列,但是一个Processor同一时刻只能执行一个G。
type p struct { id int32 status uint32 // one of pidle/prunning/... link puintptr schedtick uint32 // incremented on every scheduler call syscalltick uint32 // incremented on every system call sysmontick sysmontick // last tick observed by sysmon m muintptr // back-link to associated m (nil if idle) mcache *mcache pcache pageCache raceprocctx uintptr deferpool [5][]*_defer // pool of available defer structs of different sizes (see panic.go) deferpoolbuf [5][32]*_defer // Cache of goroutine ids, amortizes accesses to runtime·sched.goidgen. goidcache uint64 goidcacheend uint64 // Queue of runnable goroutines. Accessed without lock. runqhead uint32 runqtail uint32 runq [256]guintptr ...... } 复制代码
5.4 调度器(schedt)
schedt主要工作就是创建 g。但是创建 g 的过程非常复杂,在调度 g 之前进行了多次判断:
- 首先判断是否因为 gc 等待,如果是因为 gc 就等待 gc 结束。
- 判断是否执行安全点函数。
type schedt struct { // accessed atomically. keep at top to ensure alignment on 32-bit systems. goidgen uint64 lastpoll uint64 // time of last network poll, 0 if currently polling pollUntil uint64 // time to which current poll is sleeping lock mutex midle muintptr // idle m's waiting for work nmidle int32 // number of idle m's waiting for work nmidlelocked int32 // number of locked m's waiting for work mnext int64 // number of m's that have been created and next M ID maxmcount int32 // maximum number of m's allowed (or die) nmsys int32 // number of system m's not counted for deadlock nmfreed int64 // cumulative number of freed m's ngsys uint32 // number of system goroutines; updated atomically ... } 复制代码
5.5 相互关系
首先创建一个G对象,然后G被保存在P的本地队列或者全局队列(global queue)。这时P会唤醒一个M。P按照它的执行顺序继续执行任务。M寻找一个空闲的P,如果找得到,将G移动到它自己。然后M执行一个调度循环:调用G对象->执行->清理线程->继续寻找Goroutine。
在M的执行过程中,上下文切换随时发生。当切换发生,任务的执行现场需要被保护,这样在下一次调度执行可以进行现场恢复。M的栈保存在G对象,只有现场恢复需要的寄存器(SP,PC等),需要被保存到G对象。
如果G对象还没有被执行,M可以将G重新放到P的调度队列,等待下一次的调度执行。当调度执行时,M可以通过G的vdsoSP, vdsoPC 寄存器进行现场恢复。
参考:
www.golangroadmap.com/question_ba…
zhuanlan.zhihu.com/p/261590663