简介

goroutine是go语言中最为NB的设计,也是其魅力所在,goroutine的本质是协程,是实现并行计算的核心。goroutine使用方式非常的简单,只需使用go关键字即可启动一个协程,并且它是处于异步方式运行,你不需要等它运行完成以后在执行以后的代码。

内部原理

并发
一个cpu上能同时执行多项任务,在很短时间内,cpu来回切换任务执行(在某段很短时间内执行程序a,然后又迅速得切换到程序b去执行),有时间上的重叠(宏观上是同时的,微观仍是顺序执行),这样看起来多个任务像是同时执行,这就是并发。
并行
当系统有多个CPU时,每个CPU同一时刻都运行任务,互不抢占自己所在的CPU资源,同时进行,称为并行。
进程
cpu在切换程序的时候,如果不保存上一个程序的状态(也就是我们常说的context—上下文),直接切换下一个程序,就会丢失上一个程序的一系列状态,于是引入了进程这个概念,用以划分好程序运行时所需要的资源。因此进程就是一个程序运行时候的所需要的基本资源单位(也可以说是程序运行的一个实体)。
线程
cpu切换多个进程的时候,会花费不少的时间,因为切换进程需要切换到内核态,而每次调度需要内核态都需要读取用户态的数据,进程一旦多起来,cpu调度会消耗一大堆资源,因此引入了线程的概念,线程本身几乎不占有资源,他们共享进程里的资源,内核调度起来不会那么像进程切换那么耗费资源。
协程
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此,协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。线程和进程的操作是由程序触发系统接口,最后的执行者是系统;协程的操作执行者则是用户自身程序,goroutine也是协程。同时协程也称为微线程,它的开销比线程更小,因此更适合用来做高并发的任务

调度模型简介

image.png

groutine能拥有强大的并发实现是通过GPM调度模型实现,下面就来解释下goroutine的调度模型。
image.png
Go的调度器内部三个重要的结构:M,P,G

  • M代表内核级线程,goroutine就是跑在M之上的;M是一个很大的结构,里面维护当前执行的goroutine、随机数发生器等等非常多的信息
  • G代表一个goroutine,它有自己的栈,instruction pointer和其他信息(正在等待的channel等等),用于调度。
  • P全称是Processor,处理器,默认与机器的核心数,它的主要用途就是用来执行goroutine的,所以它也维护了一个goroutine队列,里面存储了所有需要它来执行的goroutine
  • 本地队列与全局队列:相同点:都是用来存储待运行的goroutine;不同点:本地队列有大小限制,最多能存放256个,并且在创建goroutine时回会优先存储在P的本地队列,如果P的本地队列满了,则拿一半放到全局队列中

只要goroutine执行go语句,Goroutines就会被添加到runqueue的末尾。一旦上下文运行goroutine直到调度点,它会从其runqueue中弹出goroutine,设置堆栈和指令指针并开始运行goroutine。

当一个OS线程M0陷入阻塞时,cpu执行权会立马被抢占,g是跟着p的,p被抢走了意为着p中的g也跟着过去了,但是M上是维护着一个正在处理的g,由于没了执行权,这时意为着该g是被挂起的(如下图)
image.png

当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那里要拿任务的话,一般就拿run queue的一半,这就确保了每个OS线程都能充分的使用,如下图:
image.png

特点

  • go的执行是非阻塞的
  • goroutine的执行是无序的
  • go程序运行时给main函数创建一个goroutine,遇到go关键词再创建其他的goroutine。
  • 线程是操作系统层面的多任务,而go的协程属于编译器层面的多任务,go有自己的调度器来调度。一个协程在哪个线程上是不确定的,这个是由调度器来决定的,多个协程可能在一个或多个线程上运行
  • 本地队列与全局队列的关系:相同点:都用来存放待运行的goroutine;不同点:本地队列有大小限制,并且在创建goroutine时,会优先选择

三者的宏观关系

image.png

基本使用

  1. num := runtime.NumCPU() //获取主机的逻辑CPU个数
  2. runtime.GOMAXPROCS(num) //设置可同时执行的最大CPU数
  1. func cal(a int , b int ) {
  2. c := a+b
  3. fmt.Printf("%d + %d = %d\n",a,b,c)
  4. }
  5. func main() {
  6. for i :=0 ; i<10 ;i++{
  7. go cal(i,i+1) //启动10个goroutine 来计算
  8. }
  9. time.Sleep(time.Second * 2) // sleep作用是为了等待所有任务完成
  10. }

goroutine异常捕捉

当启动多个goroutine时,如果其中一个goroutine异常了,并且我们并没有对进行异常处理,那么整个程序都会终止,所以我们在编写程序时候最好每个goroutine所运行的函数都做异常处理,异常处理采用recover

  1. func addele(a []int ,i int) {
  2. defer func() { //匿名函数捕获错误
  3. err := recover()
  4. if err != nil {
  5. fmt.Println("add ele fail")
  6. }
  7. }()
  8. a[i]=i
  9. fmt.Println(a)
  10. }
  11. func main() {
  12. Arry := make([]int,4)
  13. for i :=0 ; i<10 ;i++{
  14. go addele(Arry,i)
  15. }
  16. time.Sleep(time.Second * 2)
  17. }

goroutine泄露

业务模型:
image.png
业务需求:当run1处理完成后,run2也需要随着run1销往

线程已go程的区别:

  • 一个线程fork一个子线程,当父进程消亡,子进程也会跟着消亡
  • go程会被直接分配到groutine队列,所有的go程都是同级的,也就是说它们根本没有父子的关系

go程销毁原则

  • panic,程序出错
  • 正常的运行完
  • 手动停止
  • 如何防止泄露,一个go程A启动另一个go程B,那么A必须得负责B的关闭

功能实现:chan+for range实现

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. var mess = make(chan int,1)
  7. func Run11() {
  8. go Run22()
  9. for i:=1;i<=5;i++{
  10. mess <- i
  11. if i==3 {
  12. close(mess)
  13. break
  14. }
  15. }
  16. }
  17. func Run22(){
  18. time.Sleep(time.Millisecond*100)
  19. for res := range mess{
  20. //业务处理
  21. fmt.Println("我是run2:",res)
  22. }
  23. fmt.Println("run1退出了,我要跟着退出")
  24. }
  25. func main() {
  26. go Run11()
  27. time.Sleep(time.Second*3)
  28. }
  29. 我是run2 1
  30. 我是run2 2
  31. 我是run2 3
  32. run1退出了,我要跟着退出

功能实现:chan+for实现

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. var mess = make(chan int,1)
  7. func Run11() {
  8. go Run22()
  9. for i:=1;i<=5;i++{
  10. mess <- i
  11. if i==3 {
  12. close(mess)
  13. break
  14. }
  15. }
  16. }
  17. func Run22(){
  18. time.Sleep(time.Millisecond*100)
  19. for {
  20. res,ok:=<-mess
  21. if ok {
  22. //业务处理
  23. fmt.Println("我是run2:", res)
  24. }else {
  25. break
  26. }
  27. }
  28. fmt.Println("run1退出了,我要跟着退出")
  29. }
  30. func main() {
  31. go Run11()
  32. time.Sleep(time.Second*3)
  33. }

功能实现:context

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. "context"
  6. )
  7. func Run11() {
  8. ctx,_:=context.WithTimeout(context.TODO(),time.Nanosecond*10)
  9. go Run22(ctx)
  10. }
  11. func Run22(ctx context.Context){
  12. lable1:
  13. for {
  14. select {
  15. case <- ctx.Done():
  16. break lable1
  17. default:
  18. fmt.Println("我是run2")
  19. }
  20. }
  21. fmt.Println("run1退出了,我要跟着退出")
  22. }
  23. func main() {
  24. go Run11()
  25. time.Sleep(time.Second*3)
  26. }
  27. 我是run2
  28. 我是run2
  29. 我是run2
  30. 我是run2
  31. 我是run2
  32. run1退出了,我要跟着退出