goroutine及调度器GMP模型

协程是一种用户态轻量级线程,goroutine是go自己实现的协程,GMP则是goroutine的调度器模型。

1.什么是协程

进程是系统资源分配的最小单位, 进程的创建和销毁都是系统资源级别的,因此是一种比较昂贵的操作,进程是抢占式调度其有三个状态:等待态、就绪态、运行态。进程之间是相互隔离的,它们各自拥有自己的系统资源, 更加安全但是也存在进程间通信不便的问题。
线程是CPU调度的最小单位,进程是线程的载体容器,多个线程除了共享进程的资源还拥有自己的一少部分独立的资源,因此相比进程而言更加轻量,进程内的多个线程间的通信比进程容易,但是也同样带来了同步和互斥的问题和线程安全问题,尽管如此多线程编程仍然是当前服务端编程的主流。
协程是一种计算机程序组件,它允许暂停和恢复执行,从而可以作为通用化的非抢占式多任务处理子程序。协程在有的资料中称为微线程或者用户态轻量级线程,协程调度不需要内核参与而是完全由用户态程序来决定,因此协程对于系统而言是无感知的。协程由用户态控制就不存在抢占式调度那样强制的CPU控制权切换到其他进线程,多个协程进行协作式调度,协程自己主动把控制权转让出去之后,其他协程才能被执行到,这样就避免了系统切换开销提高了CPU的使用效率。

2.Goroutine介绍

协程是Coroutine,Go语言在语言层面对协程进行了原生支持并且称之为Goroutine。goroutine使用方式非常的简单,只需使用go关键字即可启动一个协程,并且它是处于异步方式运行,你不需要等它运行完成以后在执行以后的代码。

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

Golang以两级线程实现模型,自己实现goruntine和调度器,优势在于并行以及非常低的资源使用。

3.GMP模型

线程实现模型
线程实现模型主要分为:用户级线程模型,内核级线程模型和两级线程模型。他们的区别在于用户线程与内核线程之间的对应关系。
GMP模型
Go实现了MPG模型。GPM模型使用一种M:N的调度器来调度任意数量的协程运行于任意数量的系统线程中,从而保证了上下文切换的速度并且利用多核,但是增加了调度器的复杂度。

M:代表真正的内核OS线程,真正干活的人
G:代表一个goroutine,它有自己的栈,用于调度。
P:代表调度的上下文,可以把它看做一个局部的调度器,使go代码在一个线程上跑,它是实现从N:1到N:M映射的关键。

一个M会关联两个东西,一个是内核线程,一个是可执行的进程。
一个上下文P会有两类Goroutine,一类是正在运行的,图中绿色的G;一类是正在排队的,图中蓝色G,这个会存储在该进程中的runqueue里面。
这里的上下文P的数量也表示的是Goroutinue运行的数量,一般设置为几个,机器中就会并发运行几个。当然这里P的数量是可以设置的,通过环境变量GOMAXPROCS的值,或者通过运行时调用函数runtime.GOMAXPROCS()进行设置,最大值是256。

procs := ncpu
if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
        procs = n
}
if procresize(procs) != nil {
        throw("unknown runnable goroutine during bootstrap")
}

在这里插入图片描述

整个GPM调度的简单过程如下:

新创建的Goroutine会先存放在Global全局队列中,等待Go调度器进行调度,随后Goroutine被分配给其中的一个逻辑处理器P,并放到这个逻辑处理器对应的Local本地运行队列中,最终等待被逻辑处理器P执行即可。
在M与P绑定后,M会不断从P的Local队列中无锁地取出G,并切换到G的堆栈执行,当P的Local队列中没有G时,再从Global队列中获取一个G,当Global队列中也没有待运行的G时,则尝试从其它的P窃取部分G来执行相当于P之间的负载均衡。
Goroutine在整个生存期也存在不同的状态切换,主要的有以下几种状态:

4.示例

func action(i int) {
   fmt.Printf("Test Goroutine %d \n", i)
}

func main() {
   for i := 0; i < 10; i++ {
      go action(i)
   }
   time.Sleep(5 * time.Millisecond)
}

上面的sleep是必须的,不然你可能看不到输出或者输出不全。但上面这种写法又一个问题:如果在action中有一个执行出现panic,那整个程序就会崩掉。比如这样:

func action(i int) {
   if i == 3 {
      panic("panic 3")
   }
   fmt.Printf("Test Goroutine %d \n", i)
}

func main() {
   for i := 0; i < 100; i++ {
      go action(i)
   }
   time.Sleep(5 * time.Millisecond)
}

所以要求大家使用goroutine的时候,都需要用recover捕获异常。

func action(i int) {
   if i == 3 {
      panic("panic 3")
   }
   fmt.Printf("Test Goroutine %d \n", i)
}

func main() {
   for i := 0; i < 100; i++ {
      go func() {
         defer func() {
            if err := recover(); err != nil {
               fmt.Printf("recover panic: %+v\n", err)
            }
         }()
         action(i)
      }()
   }
   time.Sleep(5 * time.Millisecond)
}

但是上面这样,又回有另一个问题。go传给子协程的i类似一种引用传递,导致后面输出的i可能是重复的。解决方式就是手动传递参数给子协程或者在for重新定义新的变量给子协程。

func action(i int) {
   if i == 3 {
      panic("panic 3")
   }
   fmt.Printf("Test Goroutine %d \n", i)
}

func main() {
   for i := 0; i < 100; i++ {
      go func(t int) {
         defer func() {
            if err := recover(); err != nil {
               fmt.Printf("recover panic: %+v\n", err)
            }
         }()
         action(t)
      }(i)
   }
   time.Sleep(5 * time.Millisecond)
}

再看上面的代码,我们一直使用的是sleep的方方式来等待子协程结束,这种方式很不方便,因为你没有办法准确的预估子协程执行的时间,因此go提供了sync包和channel来解决同步问题。

func action(i int) {
   if i == 3 {
      panic("panic 3")
   }
   fmt.Printf("Test Goroutine %d \n", i)
}

func main() {
   var wg sync.WaitGroup
   for i := 0; i < 100; i++ {
      wg.Add(1)
      go func(t int) {
         defer func() {
            if err := recover(); err != nil {
               fmt.Printf("recover panic: %+v\n", err)
            }
            wg.Done()
         }()
         action(t)
      }(i)
   }
   //time.Sleep(5 * time.Millisecond)
   wg.Wait()
}

func action(i int) {
   if i == 3 {
      panic("panic 3")
   }
   fmt.Printf("Test Goroutine %d \n", i)
}

func main() {
   count := 10
   ch := make(chan bool, count)
   for i := 0; i < count; i++ {
      go func(t int) {
         defer func() {
            if err := recover(); err != nil {
               fmt.Printf("recover panic: %+v\n", err)
            }
            ch <- true
         }()
         action(t)
      }(i)
   }
   for i := 0; i < count; i++ {
      <-ch
   }
}

借用go channel实现PR

var (
   infos = make(chan int, 10)
)

// 生产者
func producer(index int) {
   infos <- index
}

// 消费者
func consumer(index int) {
   fmt.Printf("Consumer : %d, Receive : %d\n", index, <-infos)
}

func main() {
    // 十个生产者
    for index := 0; index < 10; index++ {
       go producer(index)
    }
    
    // 十个消费者
    for index := 0; index < 10; index++ {
       go consumer(index)
    }
    
    time.Sleep(5 * time.Millisecond)
}
参考资源

goroutine
https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-goroutine/
https://segmentfault.com/a/1190000018150987
chanel
https://colobu.com/2016/04/14/Golang-Channels/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值