1. goroutine的使用

  在Go语言中,表达式go f(x, y, z)会启动一个新的goroutine运行函数f(x, y, z),创建一个并发任务单元。即go关键字可以用来开启一个goroutine(协程))进行任务处理。

  创建单个goroutine

 package main

 import (
"fmt"
) func HelloWorld() {
fmt.Println("Hello goroutine")
} func main() {
go HelloWorld() // 开启一个新的并发运行
time.Sleep(1*time.Second)
fmt.Println("后输出消息!")
}

  输出

 Hello goroutine
后输出消息!

  这里的sleep是必须的,否则你可能看不到goroutine里头的输出,或者里面的消息后输出。因为当main函数返回时,所有的gourutine都是暴力终结的,然后程序退出。

  创建多个goroutine时

 package main

 import (
"fmt"
"time"
) func DelayPrint() {
for i := ; i <= 3; i++ {
time.Sleep( * time.Millisecond)
fmt.Println(i)
}
} func HelloWorld() {
fmt.Println("Hello goroutine")
} func main() {
go DelayPrint() // 第一个goroutine
go HelloWorld() // 第二个goroutine
time.Sleep(*time.Second)
fmt.Println("main func")
}

  输出

 Hello  goroutine

 main func

  当去掉 DelayPrint() 函数里的sleep之后,输出为:


 Hello goroutine
main function

  说明第二个goroutine不会因为第一个而堵塞或者等待。事实是当程序执行go FUNC()的时候,只是简单的调用然后就立即返回了,并不关心函数里头发生的故事情节,所以不同的goroutine直接不影响,main会继续按顺序执行语句。

goroutine阻塞

  场景一:

 package main

 func main() {
ch := make(chan int)
<- ch // 阻塞main goroutine, 通道被锁
}

  运行程序会报错:

 fatal error: all goroutines are asleep - deadlock!

 goroutine  [chan receive]:
main.main()

  场景二

 package main

 func main() {
ch1, ch2 := make(chan int), make(chan int) go func() {
ch1 <- // ch1通道的数据没有被其他goroutine读取走,堵塞当前goroutine
ch2 <-
}() <- ch2 // ch2 等待数据的写
}

  非缓冲通道上如果只有数据流入,而没有流出,或者只流出无流入,都会引起阻塞。 goroutine的非缓冲通道里头一定要一进一出,成对出现。 上面例子,一:流出无流入;二:流入无流出。

  处理方式:

  1. 读取通道数据

 package main

 func main() {
ch1, ch2 := make(chan int), make(chan int) go func() {
ch1 <- // ch1通道的数据没有被其他goroutine读取走,堵塞当前goroutine
ch2 <-
}() <- ch1 // 取走便是
<- ch2 // chb 等待数据的写
}

  2. 创建缓冲通道

 package main

 func main() {
ch1, ch2 := make(chan int, ), make(chan int) go func() {
ch1 <- // cha通道的数据没有被其他goroutine读取走,堵塞当前goroutine
ch2 <-
}() <- ch2 // ch2 等待数据的写
}

2. goroutine调度器相关结构

  goroutine的调度涉及到几个重要的数据结构,我们先逐一介绍和分析这几个数据结构。这些数据结构分别是结构体G,结构体M,结构体P,以及Sched结构体。前三个的定义在文件runtime/runtime.h中,而Sched的定义在runtime/proc.c中。Go语言的调度相关实现也是在文件proc.c中。

2.1 结构体G

  g是goroutine的缩写,是goroutine的控制结构,是对goroutine的抽象。看下它内部主要的一些结构:

 type g struct {
//堆栈参数。
     //堆栈描述了实际的堆栈内存:[stack.lo,stack.hi)。
     // stackguard0是在Go堆栈增长序言中比较的堆栈指针。
     //通常是stack.lo + StackGuard,但是可以通过StackPreempt触发抢占。
     // stackguard1是在C堆栈增长序言中比较的堆栈指针。
     //它是g0和gsignal堆栈上的stack.lo + StackGuard。
     //在其他goroutine堆栈上为〜0,以触发对morestackc的调用(并崩溃)。
//当前g使用的栈空间,stack结构包括 [lo, hi]两个成员
stack stack // offset known to runtime/cgo
// 用于检测是否需要进行栈扩张,go代码使用
stackguard0 uintptr // offset known to liblink
// 用于检测是否需要进行栈扩展,原生代码使用的
stackguard1 uintptr // offset known to liblink
// 当前g所绑定的m
m *m // current m; offset known to arm liblink
// 当前g的调度数据,当goroutine切换时,保存当前g的上下文,用于恢复
sched gobuf
// goroutine运行的函数
fnstart *FuncVal
// g当前的状态
atomicstatus uint32
// 当前g的id
goid int64
// 状态Gidle,Grunnable,Grunning,Gsyscall,Gwaiting,Gdead
status int16
// 下一个g的地址,通过guintptr结构体的ptr set函数可以设置和获取下一个g,通过这个字段和sched.gfreeStack sched.gfreeNoStack 可以把 free g串成一个链表
schedlink guintptr
// 判断g是否允许被抢占
preempt bool // preemption signal, duplicates stackguard0 = stackpreempt
// g是否要求要回到这个M执行, 有的时候g中断了恢复会要求使用原来的M执行
lockedm muintptr
// 用于传递参数,睡眠时其它goroutine设置param,唤醒时此goroutine可以获取
param *void
// 创建这个goroutine的go表达式的pc
uintptr gopc
}

  其中包含了栈信息stackbase和stackguard,有运行的函数信息fnstart。这些就足够成为一个可执行的单元了,只要得到CPU就可以运行。goroutine切换时,上下文信息保存在结构体的sched域中。goroutine切换时,上下文信息保存在结构体的sched域中。goroutine是轻量级的线程或者称为协程,切换时并不必陷入到操作系统内核中,很轻量级。

  结构体G中的Gobuf,其实只保存了当前栈指针,程序计数器,以及goroutine自身。

 struct Gobuf
{
//这些字段的偏移是libmach已知的(硬编码的)。
sp uintper;
pc *byte;
g *G;
...
};

  记录g是为了恢复当前goroutine的结构体G指针,运行时库中使用了一个常驻的寄存器extern register G* g,这是当前goroutine的结构体G的指针。这种结构是为了快速地访问goroutine中的信息,比如,Go的栈的实现并没有使用%ebp寄存器,不过这可以通过g->stackbase快速得到。"extern register"是由6c,8c等实现的一个特殊的存储,在ARM上它是实际的寄存器。在linux系统中,对g和m使用的分别是0(GS)和4(GS)。链接器还会根据特定操作系统改变编译器的输出,每个链接到Go程序的C文件都必须包含runtime.h头文件,这样C编译器知道避免使用专用的寄存器。

2.2 结构体P

  P是Processor的缩写。结构体P的加入是为了提高Go程序的并发度,实现更好的调度。M代表OS线程。P代表Go代码执行时需要的资源。

 type p struct {
lock mutex id int32
// p的状态,稍后介绍
status uint32 // one of pidle/prunning/... // 下一个p的地址,可参考 g.schedlink
link puintptr
// p所关联的m
m muintptr // back-link to associated m (nil if idle) // 内存分配的时候用的,p所属的m的mcache用的也是这个
mcache *mcache // Cache of goroutine ids, amortizes accesses to runtime·sched.goidgen.
// 从sched中获取并缓存的id,避免每次分配goid都从sched分配
goidcache uint64
goidcacheend uint64 // Queue of runnable goroutines. Accessed without lock.
// p 本地的runnbale的goroutine形成的队列
runqhead uint32
runqtail uint32
runq []guintptr // runnext,如果不是nil,则是已准备好运行的G
   //当前的G,并且应该在下一个而不是其中运行
   // runq,如果运行G的时间还剩时间
   //切片。它将继承当前时间剩余的时间
   //切片。如果一组goroutine锁定在
   //交流等待模式,该计划将其设置为
   //单位并消除(可能很大)调度
   //否则会由于添加就绪商品而引起的延迟
   // goroutines到运行队列的末尾。 // 下一个执行的g,如果是nil,则从队列中获取下一个执行的g
runnext guintptr // Available G's (status == Gdead)
// 状态为 Gdead的g的列表,可以进行复用
gfree *g
gfreecnt int32
}

  跟G不同的是,P不存在waiting状态。MCache被移到了P中,但是在结构体M中也还保留着。在P中有一个Grunnable的goroutine队列,这是一个P的局部队列。当P执行Go代码时,它会优先从自己的这个局部队列中取,这时可以不用加锁,提高了并发度。如果发现这个队列空了,则去其它P的队列中拿一半过来,这样实现工作流窃取的调度。这种情况下是需要给调用器加锁的。

2.3 结构体M

  M是machine的缩写,是对机器的抽象,每个m都是对应到一条操作系统的物理线程。

 type m struct {
// g0是用于调度和执行系统调用的特殊g
g0 *g // goroutine with scheduling stack
// m当前运行的g
curg *g // current running goroutine
// 当前拥有的p
p puintptr // attached p for executing go code (nil if not executing go code)
// 线程的 local storage
tls []uintptr // thread-local storage
// 唤醒m时,m会拥有这个p
nextp puintptr
id int64
// 如果 !="", 继续运行curg
preemptoff string // if != "", keep curg running on this m
// 自旋状态,用于判断m是否工作已结束,并寻找g进行工作
spinning bool // m is out of work and is actively looking for work
// 用于判断m是否进行休眠状态
blocked bool // m is blocked on a note
// m休眠和唤醒通过这个,note里面有一个成员key,对这个key所指向的地址进行值的修改,进而达到唤醒和休眠的目的
park note
// 所有m组成的一个链表
alllink *m // on allm
// 下一个m,通过这个字段和sched.midle 可以串成一个m的空闲链表
schedlink muintptr
// mcache,m拥有p的时候,会把自己的mcache给p
mcache *mcache
// lockedm的对应值
lockedg guintptr
// 待释放的m的list,通过sched.freem 串成一个链表
freelink *m // on sched.freem
}

  和G类似,M中也有alllink域将所有的M放在allm链表中。lockedg是某些情况下,G锁定在这个M中运行而不会切换到其它M中去。M中还有一个MCache,是当前M的内存的缓存。M也和G一样有一个常驻寄存器变量,代表当前的M。同时存在多个M,表示同时存在多个物理线程。

2.4 Sched结构体

  Sched是调度实现中使用的数据结构,该结构体的定义在文件proc.c中。

 type schedt struct {
// 全局的go id分配
goidgen uint64
// 记录的最后一次从i/o中查询g的时间
lastpoll uint64 lock mutex //当增加nmidle,nmidlelocked,nmsys或nmfreed时,应
   //确保调用checkdead()。 // m的空闲链表,结合m.schedlink 就可以组成一个空闲链表了
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
// 下一个m的id,也用来记录创建的m数量
mnext int64 // number of m's that have been created and next M ID
// 最多允许的m的数量
maxmcount int32 // maximum number of m's allowed (or die)
nmsys int32 // number of system m's not counted for deadlock
// free掉的m的数量,exit的m的数量
nmfreed int64 // cumulative number of freed m's ngsys uint32 // 系统goroutine的数量;原子更新 pidle puintptr // 闲置的
npidle uint32
nmspinning uint32 // See "Worker thread parking/unparking" comment in proc.go. // Global runnable queue.
// 这个就是全局的g的队列了,如果p的本地队列没有g或者太多,会跟全局队列进行平衡
// 根据runqhead可以获取队列头的g,然后根据g.schedlink 获取下一个,从而形成了一个链表
runqhead guintptr
runqtail guintptr
runqsize int32 // freem是m等待被释放时的列表
   //设置了m.exited。通过m.freelink链接。 // 等待释放的m的列表
freem *m
}

  大多数需要的信息都已放在了结构体M、G和P中,Sched结构体只是一个壳。可以看到,其中有M的idle队列,P的idle队列,以及一个全局的就绪的G队列。Sched结构体中的Lock是非常必须的,如果M或P等做一些非局部的操作,它们一般需要先锁住调度器。

3. G、P、M相关状态

g.status

  • _Gidle: goroutine刚刚创建还没有初始化
  • _Grunnable: goroutine处于运行队列中,但是还没有运行,没有自己的栈
  • _Grunning: 这个状态的g可能处于运行用户代码的过程中,拥有自己的m和p
  • _Gsyscall: 运行systemcall中
  • _Gwaiting: 这个状态的goroutine正在阻塞中,类似于等待channel
  • _Gdead: 这个状态的g没有被使用,有可能是刚刚退出,也有可能是正在初始化中
  • _Gcopystack: 表示g当前的栈正在被移除,新栈分配中

goroutine的状态变化

  在newproc1中新建的goroutine被设置为Grunnable状态,投入运行时设置成Grunning。Grunning状态的goroutine会在entersyscall的时候goroutine的状态被设置为Gsyscall,到出系统调用时根据它是从阻塞系统调用中出来还是非阻塞系统调用中出来,又会被设置成Grunning或者Grunnable的状态。在goroutine最终退出的runtime.exit函数中,goroutine被设置为Gdead状态。还会在进行I/O时可能会进入waiting状态,主动让出CPU,此时会被移到所属P中的其他G后面,等待下一次轮到执行。

p.status

  • _Pidle: 空闲状态,此时p不绑定m
  • _Prunning: m获取到p的时候,p的状态就是这个状态了,然后m可以使用这个p的资源运行g
  • _Psyscall: 当go调用原生代码,原生代码又反过来调用go的时候,使用的p就会变成此态
  • _Pdead: 当运行中,需要减少p的数量时,被减掉的p的状态就是这个了

m.status

m的status没有p、g的那么明确,但是在运行流程的分析中,主要有以下几个状态

  • 运行中: 拿到p,执行g的过程中
  • 运行原生代码: 正在执行原声代码或者阻塞的syscall
  • 休眠中: m发现无待运行的g时,进入休眠,并加入到空闲列表中
  • 自旋中(spining): 当前工作结束,正在寻找下一个待运行的g

4. G、P、M的调度关系

  一个G就是一个gorountine,保存了协程的栈、程序计数器以及它所在M的信息。P全称是Processor,处理器,它的主要用途就是用来执行goroutine的。M代表内核级线程,一个M就是一个线程,goroutine就是跑在M之上的。程序启动时,会创建一个主G,而每使用一次go关键字也创建一个G。go func()创建一个新的G后,放到P的本地队列里,或者平衡到全局队列,然后检查是否有可用的M,然后唤醒或新建一个M,M获取待执行的G和空闲的P,将调用参数保存到g的栈,将sp,pc等上下文环境保存在g的sched域,这样整个goroutine就准备好了,只要等分配到CPU,它就可以继续运行,之后再清理现场,重新进入调度循环。

4.1 调度实现

  图中有两个物理线程,M0、M1每一个M都拥有一个处理器P,每一个P都有一个正在运行的G。P的数量可以通过GOMAXPROCS()来设置,它其实也代表了真正的并发度,即有多少个goroutine可以同时运行。图中灰色goroutine都是处于ready的就绪态,正在等待被调度。由P维护这个就绪队列(runqueue),go function每启动一个goroutine,runqueue队列就在其末尾加入一个goroutine,在下一个调度点,就从runqueue中取出一个goroutine执行。

  当一个OS线程M0陷入阻塞时,P转而在M1上运行G,图中的M1可能是正被创建,或者从线程缓存中取出。当MO返回时,它尝试取得一个P来运行goroutine,一般情况下,它会从其他的OS线程那里拿一个P过来执行,像M1获取P一样;如果没有拿到的话,它就把goroutine放在一个global runqueue(全局运行队列)里,然后自己睡眠(放入线程缓存里)。所有的P会周期性的检查全局队列并运行其中的goroutine,否则其上的goroutine永远无法执行。

  另一种情况是P上的任务G很快就执行完了(分配不均),这个处理器P很忙,但是其他的P还有任务,此时如果global runqueue也没有G了,那么P就会从其他的P里拿一些G来执行。一般来说,如果一般就拿run queue的一半,这就确保了每个OS线程都能充分的使用。

  golang采用了m:n线程模型,即m个gorountine(简称为G)映射到n个用户态进程(简称为P)上,多个G对应一个P,一个P对应一个内核线程(简称为M)。

4.2 P、M的数量

  P的数量:由启动时环境变量$GOMAXPROCS或者是由runtime的方法GOMAXPROCS()决定(默认是1)。这意味着在程序执行的任意时刻都只有$GOMAXPROCS个goroutine在同时运行。在确定了P的最大数量n后,运行时系统会根据这个数量创建n个P。

  M的数量:go语言本身的限制:go程序启动时,会设置M的最大数量,默认10000.但是内核很难支持这么多的线程数,所以这个限制可以忽略。runtime/debug中的SetMaxThreads函数,设置M的最大数量。一个M阻塞了,会创建新的M。

  M与P的数量没有绝对关系,一个M阻塞,P就会去创建或者切换另一个M,所以,即使P的默认数量是1,也有可能会创建很多个M出来。

4.2 P、G的调度细节

  P上G的调度:如果一个G不主动让出cpu或被动block,所属P中的其他G会一直等待顺序执行。

  一个G执行IO时可能会进入waiting状态,主动让出CPU,此时会被移到所属P中的其他G后面,等待下一次轮到执行。
  一个G调用了runtime.Gosched()会进入runnable状态,主动让出CPU,并被放到全局等待队列中。
  一个G调用了runtime.Goexit(),该G将会被立即终止,然后把已加载的defer(有点类似析构)依次执行完。
  一个G调用了允许block的syscall,此时G及其对应的P、其他G和M都会被block起来,监控线程M会定时扫描所有P,一旦发现某个P处于block syscall状态,则通知调度器让另一个M来带走P(这里的另一个M可能是新创建的,因此随着G被不断block,M数量会不断增加,最终M数量可能会超过P数量),这样P及其余下的G就不会被block了,等被block的M返回时发现自己的P没有了,也就不能再处理G了,于是将G放入全局等待队列等待空闲P接管,然后M自己sleep。
  通过实验,当一个G运行了很久(比如进入死循环),会被自动切到其他CPU核,可能是因为超过时间片后G被移到全局等待队列中,后面被其他CPU核上的M处理。

  M上P和G的调度:每当一个G要开始执行时,调度器判断当前M的数量是否可以很好处理完G:如果M少G多且有空闲P,则新建M或唤醒一个sleep M,并指定使用某个空闲P;如果M应付得来,G被负载均衡放入一个现有P+M中。

  当M处理完其身上的所有G后,会再去全局等待队列中找G,如果没有就从其他P中分一半的G(以便保证各个M处理G的负载大致相等),如果还没有,M就去sleep了,对应的P变为空闲P。
在M进入sleep期间,调度器可能会给其P不断放入G,等M醒后(比如超时):如果G数量不多,则M直接处理这些G;如果M觉得G太多且有空闲P,会先主动唤醒其他sleep的M来分担G,如果没有其他sleep的M,调度器创建新M来分担。

协程特点

  协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此,协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。线程和进程的操作是由程序触发系统接口,最后的执行者是系统;协程的操作执行者则是用户自身程序,goroutine也是协程。

go中的关键字-go(上)的更多相关文章

  1. js中this关键字测试集锦

    参考:阮一峰<javascript的this用法>及<JS中this关键字详解> this是Javascript语言的一个关键字它代表函数运行时,自动生成的一个内部对象,只能在 ...

  2. 【转载】C/C++中extern关键字详解

    1 基本解释:extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义.此外extern也可用来进行链接指定. 也就是说extern ...

  3. 深入解析Javascript中this关键字的使用

    深入解析Javascript中面向对象编程中的this关键字 在Javascript中this关键字代表函数运行时,自动生成的一个内部对象,只能在函数内部使用.比如: function TestFun ...

  4. C/C++中extern关键字解析

    1 基本解释:extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义.此外extern也可用来进行链接指定. 也就是说extern ...

  5. C++中typename关键字的用法

    我在我的 薛途的博客 上发表了新的文章,欢迎各位批评指正. C++中typename关键字的用法

  6. 【有人@我】Android中高亮变色显示文本中的关键字

    应该是好久没有写有关技术类的文章了,前天还有人在群里问我,说群主很长时间没有分享干货了,今天分享一篇Android中TextView在大段的文字内容中如何让关键字高亮变色的文章 ,希望对大家有所帮助, ...

  7. C/C++中static关键字详解

    静态变量作用范围在一个文件内,程序开始时分配空间,结束时释放空间,默认初始化为0,使用时可以改变其值. 静态变量或静态函数只有本文件内的代码才能访问它,它的名字在其它文件中不可见.用法1:函数内部声明 ...

  8. C/C++中extern关键字详解

    转自:http://www.cnblogs.com/yc_sunniwell/archive/2010/07/14/1777431.html 1 基本解释:extern可以置于变量或者函数前,以标示变 ...

  9. PostgreSQL中关于关键字(保留字)在表名和字段名中的应用文件解决

    标识符和关键词 受限标识符或被引号修饰的标识符.它是由双引号(")包围的一个任意字符序列.一个受限标识符总是一个标识符而不会是一个关键字.因此"select"可以用于引用 ...

随机推荐

  1. 玩转 RTC时钟库 DS1302

    1.前言     最近博主在弄8266编程的时候,偶然发现两个全新时钟模块压仓货: DS1302 DS3231     为了避免资源浪费以及重复编写代码,博主还是抱着尝试的心态去寻找能够同时兼容 DS ...

  2. Arduino学习笔记⑦ EEPROM断电保存数据

    1.前言     EEPROM,叫做电可擦可编程可读寄存器(是不是觉得好官方,不知道是什么鬼?反正我也一脸懵逼),只需要知道这是一种断电后数据不会丢失的存储设备,可以用来应对需要做记录做保存的场合.简 ...

  3. docker-compose下的java应用启动顺序两部曲之一:问题分析

    在docker-compose编排多个容器时,需要按实际情况控制各容器的启动顺序,本文是<docker-compose下的java应用启动顺序两部曲>的第一篇,文中会分析启动顺序的重要性, ...

  4. Xbim.GLTF源码解析(三):Builder类

    原创作者:flowell,转载请标明出处:https://www.cnblogs.com/flowell/p/10838706.html IFC提取转换成GLTF的逻辑在Builder类中, Buil ...

  5. 关于dt分组、计数、排序的实例

    #region table去重复求和 var query = dt.Rows.Cast<DataRow>() .OrderByDescending(n => n["OPER ...

  6. Vue---mock.js 使用

    mockjs 概述 在我们的生产实际中,后端的接口往往是较晚才会出来,并且还要写接口文档,于是我们的前端的许多开发都要等到接口给我们才能进行,这样对于我们前端来说显得十分的被动,于是有没有可以制造假数 ...

  7. SpringBoot2.1.9+dubbo2.7.3+Nacos1.1.4构建你的微服务体系

    简单几步使用最新版本的DUBBO构建你的微服务体系 NACOS注册中心 从github下载最新版本的nacos 上传至服务器并解压 单机启动sh startup.sh -m standalone na ...

  8. 根据多个成对的cron表达式生成的时间段,合并

    场景:数据库一张表,有个startcron 和endcron 两个字段,根据表达式计算今天的所有时间段. 例:startcron :0 30 20 ? * * endcron :0 30 21 ? * ...

  9. window10系统下,彻底删除卸载mysql

    本文介绍,在Windows10系统下,如何彻底删除卸载MySQL...1>停止MySQL服务开始->所有应用->Windows管理工具->服务,将MySQL服务停止.2> ...

  10. 还不会用FindBugs?你的代码质量很可能令人堪忧

    前言 项目中代码质量,往往需要比较有经验的程序员的审查来保证.但是随着项目越来越大,代码审查会变得越来越复杂,需要耗费越来越多的人力.而且程序员的经验和精力都是有限的,能审查出问题必定有限.而在对代码 ...