goroutine 调度原理

原文转载于 https://www.cnblogs.com/wdliu/p/9272220.html
go routine 调度
一、goroutine 简介 goroutine 本质是协程,并行计算的核心。goroutine使用方式非常简单,只需要go关键字即可启动一个协程,并且它是处于异步方式执行,并不需要等他运行完成以后在执行以后的代码

go func() { .... }() //通过go关键字启动一个协程来运行函数

二、 go routine 内部原理 概念介绍 在进行原理之前,先了解关键术语的概念
并发一个cpu上面能同时执行多项任务,在短时间内,cpu来回切换任务执行(在短时间内执行a,然后又迅速的切换到b执行),宏观上是同时的,微观上是顺序执行的,看起来像是多个任务同时执行,这就是并发。
并行 系统有多个cpu每个cpu同一个时刻都运行任务,互不抢占自己所在的cpu的资源,同时进行,称为并行
进程 cpu在切换程序的时候,如果不保存上一个程序的状态,(我们所说的context–上下文),直接切换下一个程序,就会丢失上一个程序的一系列状态,于是引入了进程这个概念,用以划分好程序运行时所需要的资源。因此进程就是一个程序运行时侯所需要的基本资源单位(程序运行时的一个实体)。-----运行时保存有一系列资源的可执行程序实体
线程 cpu切换多个进程的时候,会花费不少时间,因为切换进程需要切换到内核态,而每次调度需要内核态都读取用户态的数据,进程一旦多起来,cpu调度会消耗一大堆资源,因此引入了线程的概念,线程本身几乎不占有资源,他们共享进程里面的资源,内核调度起来不会那么像进程切换那么消耗资源。
协程 协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此,协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。线程和进程的操作是由程序触发系统接口,最后的执行者是系统;协程的操作执行者是用户自身程序,goroutine是协程。
调度模型简介 goroutine 能够拥有强大的并发是通过gpm调度模型实现。
M P G go的调度器内部有四个重要的结构:M,P,S,Sched
M: M代表内核级的线程,一个M是一个线程,goroutine是跑在M之上的;M是一个很大的结构,里面维护小对象内存cache(mcache),当执行goroutine,随机数发生器等等非常多的信息。
G:代表一个goroutine ,他有自己的栈,instruction pointer 和其他信息(正在等待的channel等等),用于调度。
P:P全称是Processor处理器,他的主要用途是用来执行goroutine的,他维护了一个goroutine队列,里面存储了所有需要它来执行的goroutine
Sched :代表调度器,他维护有存储M和G的队列以及调度器的一些状态信息等。
调度实现 goroutine 调度原理
文章图片

从上图中看,有两个物理线程M,每一个M都拥有一个处理器P,每一个都有一个正在运行的goroutine。
P的数量可以通过 GMAXPROCS()来设置,它其实也就代表了真正的并发度,即有多少个goroutine可以同时运行。
图中灰色的goroutine并没有运行,而是处于ready的就绪态,正在等待被调度。P维护着这个队列。(称之为runqueue)
go语言里,启动一个goroutine很容易:go func(){…}()就行,所以每有一个go语句被执行,runqueue就在队列末尾加入一个
在下一个调度点,goroutine 从runqueue中取出来(现在可以认为时随机取出来)并执行。
当一个os线程MO陷入阻塞时(如下图),P转而运行M1,图中M1可能正是被创建,或者从线程缓存取出。
goroutine 调度原理
文章图片

当MO返回时候,他必须尝试取得一个P来运行goroutine,一般情况下,它会从其他的os线程那里拿一个P过来。
如果没有拿到的话,就把goroutine放在一个global runqueue里面,然后自己睡眠(放入线程缓存里面)。所有的P也会周期性的检查global runqueue 并运行其中的goroutine,否则global runqueue上的goroutine永远无法执行
另一种情况是p所分配的任务G很快就执行完了。(分配不均)这就导致了这个处理器p很忙,但是其他的p还有任务,此时如果global runqueue 没有任务G了,那么P不得不从其他的P那里拿来一些G来执行。一般来说,如果P从其他的P那里要拿任务的化,一般就 拿runqueue 的一半,确保每个os线程都能充分的使用。如下图
goroutine 调度原理
文章图片

三、使用goroutine 基本使用 设置goroutine运行的cpu数量,最新版本的go已经默认设置了
num := runtime.NumCPU() //获取主机的逻辑cpu的个数 runtime.GOMAXPROCS(num) //设置可同时执行的最大cpu个数

使用实例
package main import( "fmt" "time" ) func cal(a int,b int){ c:=a+b fmt.Printf("%d + %d = %d \n",a,b,c) } func main(){ for i:=0; i<10; i++{ go cal(i,i+1) //启动10个goroutine来计算 } time.Sleep(time.Second*2) //sleep是为了等待所有任务完成 }

goroutine 异常捕捉 当启动多个goroutine时,如果其中一个goroutine异常了,并且我们没有进行异常处理,那么整个程序都会终,所以我们在编写程序的时候最好每个roroutine所运行的函数都进行异常处理,异常处理采用recover
package main import( "fmt" "time" ) func addele(a []int ,i int){ defer func(){ //匿名函数捕获错误 err:=recover() if err!=nil{ fmt.Println("add ele fail") } }() a[i]=i fmt.Println(a) }func main(){ Array:=make([]int a) for i:=0; i<10; i++{ go addele(Array,i) } time.Sleep(time.Second*2) } //结果 add ele fail [0 0 0 0] [0 1 0 0] [0 1 2 0] [0 1 2 3] add ele fail add ele fail add ele fail add ele fail add ele fail

同步的goroutine 由于goroutine是异步执行的,那很有可能出现主程序 退出时还有goroutine没有执行完,此时goroutine也会跟着退出。如果想等到所有的 goroutine都执行完毕之后再退出程序,go提供了sync包 和channel来解决同步问题,当然如果你能 预测每个goroutine的执行时间,你还可以通过time.Sleep方式等待所有的goroutine执行完成以后在退出程序(如上面的例子)
示例一:使用sycn包同步goroutine
sync大致实现方式
WaitGroup等待一组goroutine执行完毕,主程序调用Add添加等待的goroutine的数量,每个goroutine的执行在结束时调用Done,此时 等待队列数量减1,主程序通过Wait阻塞,直到等待队列为0
package main import ( "fmt" "sync" ) func cal(a int,b int,n* sync.WaitGroup){ c:=a+b fmt.Printf("%d + %d = %d\n",a,b,c) defer n.Done() //goroutine完成后,WaitGroup的计数-1 }func main(){ var go_sync sync.WaitGroup //声明一个WaitGroup变量 for i:=0; i<10; i++{ go_sync.Add(1) //WaitGroup 的计数加1 go cal(i,i+1,&go_sync) } go_sync.Wait() //等待所有的goroutine执行完毕 }

示例二:通过channel实现goroutine之间的同步
实现方式:通过channel能在 多个goroutine之间 通讯,当一个goroutine完成时候 向channel发送 退出信号,等待所有的 goroutine退出的时候,利用for循环 channel取channel中的信号 ,若取不到数据就会阻塞的原理,等待所有的goroutine执行完毕,使用该方法 有个前提是你已经知道了启动了多少个goroutine
package main import( "fmt" "time" ) func cal(a int ,b int,Exitchan chan bool){ c:=a+b fmt.Printf("%d + %d =%d \n",a,b,c) time.Sleep(time.Second*2) Exitchan <-true } func main(){ Exitchan :=make(chan bool,10) for i:=0; i<10; i++{ go cal(i,i+1,Exitchan) } for j:=0; j<10; j++{ <-Exitchan //取信号,如果取不到会阻塞 } close(Exitchan)//关闭管道 }

goroutine之间的通讯 goroutine本质上是协程,可以理解为不受内核调度,而受go调度器管理 的 线程。goroutine之间可以通过channel进行通讯,或者实现数据共享,当然也可以使用全局变量来进行数据共享。
示例:使用channel模拟生产者和消费者模式
package main import( "fmt" "sync" ) func Productor(mychan chan int ,data int ,wait *sync.WaitGroup){ mychan<-data fmt.Println("product data:",data) wait.Done() } func Consumer(mychan chan int,wait *sync.WaitGroup){ a:=<-mychan fmt.Println("consumer data",a) wait.Done() }func main(){ datachan := make(chan int,100) //通讯数据管道 var wg sync.WaitGroup fori:=0; i<10; i++{ go Productor(datachan,i,&wg) //生产数据 wg.Add(1) } for j:=0; j<10; j++{ go Consumer(datachan,&wg) //消费数据 wg.Add(1) } wg.Wait() }

四、channel 简介 channel俗称管道,用于数据传递或 数据共享,其本质 是一个先进先出队列,使用goroutine+channel进行数据通讯简单高效,同时也线程安全,多个goroutine可同时 修改一个channel,不需要 加锁。
chnnel 可分为三种模型:
只读channel:只能读里面的数据不可以写入
只写channel:只能写数据,不可读
一般channel:可读可写
channel使用 【goroutine 调度原理】定义和声明
var readOnlyChan <-chan int//只读channel var writeOnlyChan char<- int //只写channel var mychan chan int //读写channel //定义完成以后需要 make来分配内存空间,不然使用会dedlock machannel = make(chan int,10) //或者 read_only := make (<-chan int,10)//定义只读的channel write_only := make (chan<- int,10)//定义只写的channel read_write := make (chan int,10)//可同时读写

读写数据
需要 注意的是:
  • 管道如果未关闭,在 读取超时则会发生deadlock异常
  • 管道如果 关闭 写入数据会pcnic
  • 当管道中没有数据时候 再行读取或读取到默认值,如int类型默认值 是0
    ch<- "wd" //写数据 a:=<-ch //读取 数据 a,ok :=<-ch //优雅的读取 数据

    循环管道
    需要注意的是:
    • 使用range循环管道,如果管道未关闭会引发deadlock错误
    • 如果采用for死循环已经关闭的管道,当管道没有数据的 时候读取的数据会是管道的默认值,并且循环不会退出。
packagemain import( "fmt" "time" ) func main(){ mychannel :=make(chan int,10) for i:=0; i<10; i++{ mychannel<- i } close(mychannel)//关闭 管道 fmt.Println("data length:",len(mychannel)) for v:=range mychannel{ fmt.Println(v) } fmt.Printf("data length: %d",len(mychannel)) }

带缓冲区和不带有缓冲区 带缓冲区channel:定义声明时候 制定了缓冲区大小(长度),可以保存 多个数据。
不带 缓冲区:只能存一个数据,并且只有当该数据被读取出来的 时候才能存下一个数据
ch:=make(chan int) //不带缓冲区 ch:=make(chan int,10) //带缓冲去

不带缓冲区示例:
package main import "fmt" func test(c chan int){ for i:=0; i<10; i++{ fmt.Println("send",i) c<- i } } func main(){ ch:=make(chan int) go test(ch) for j:=0; j<10; j++{ fmt.Println("get",<-ch) } }

channel 实现作业池 我们 创建三个channel ,一个channel用于接受任务,一个channel用于保持结果,还有一个channel用于决定程序退出的时候。
package main import( "fmt" ) func Storesult(task,res chan int,exitch chan bool){ defer func(){ err:=recover() if err!=nil{ fmt.Println("do task error",err) return } }() for t:=range task{ //处理任务 fmt.Println("do task:",t) res<-t } exitch<- true //处理完发送退出信号 } func main(){ task :=maek(chan int ,20)//任务管道 res :=make(chan int ,20)//结果管道 exitchan :=make(chan bool,5) go func(){ for i:=0; i<10; i++{ task<-i } close(task) }() for i:=0; i<5; i++{ //启动五个goroutine做任务 go Storesult(task,res,exitchan) } go func(){//等5个goroutine结果 for i:=0; i<5; i++{ <-exitch } close(resch)//任务处理完成关闭结果管道,不然range报错 close(exitch)//关闭退出管道 } for re:=range res{ fmt.Println("task res:",re) } }

只读channel和只写channel 一般定义只读和只写管道意义不大,跟多时候我们可以在 参数传递的时候指明管道 可读还是可写,即使当前管道是可读写 的 。
package mainimport ( "fmt" "time" )//只能向chan里写数据 func send(c chan<- int) { for i := 0; i < 10; i++ { c <- i } } //只能取channel中的数据 func get(c <-chan int) { for i := range c { fmt.Println(i) } } func main() { c := make(chan int) go send(c) go get(c) time.Sleep(time.Second*1) }

select-case 实现非阻塞channel 原理通过select+case 加入一组管道,当满足select中的某个case的时候,那么case返回,若都不满足case,则走default分支。
package mainimport ( "fmt" )func send(c chan int){ for i :=1 ; i<10 ; i++{ c <-i fmt.Println("send data : ",i) } }func main() { resch := make(chan int,20) strch := make(chan string,10) go send(resch) strch <- "wd" select { case a := <-resch: fmt.Println("get data : ", a) case b := <-strch: fmt.Println("get data : ", b) default: fmt.Println("no channel actvie")}}

channel 频率控制 在对channel进行读写的时候,go还提供了非常人性化的操作,就是对读写的频率控制,通过time.Ticke实现
package mainimport ( "time" "fmt" )func main(){ requests:= make(chan int ,5) for i:=1; i<5; i++{ requests<-i } close(requests) limiter := time.Tick(time.Second*1) for req:=range requests{ <-limiter fmt.Println("requets",req,time.Now()) //执行到这里,需要隔1秒才继续往下执行,time.Tick(timer)上面已定义 } }

    推荐阅读