Golang调度原理-浅析

Golang调度原理

1.进程和线程的区别

请看下面这篇文章:

https://zhuanlan.zhihu.com/p/520460326

看完之后,为什么发明线程?

多线程并不能提高运行速度,但可以提高运行效率,让CPU的使用率更高

2.线程和协程的区别

问题1:线程是CPU调度的最小单位,同一个进程内有多个线程,CPU最多只能看到线程,协程在CPU如何运行的?

协程在CPU如何运行的?

答:Golang的协程是由Go调度器进行管理和调度的,调度器会将多个协程映射到少量的操作系统线程上执行。最终还是要在线程执行的。

线程和协程区别1:线程是CPU调度的,Go调度器进行管理和调度的

那为什么要多次一举,干嘛不直接运行线程的?

因为在很多线程的情况下,线程之间切换很浪费时间。而且多线程开发时候,设计会变得更加复杂,要考虑很多同步竞争等问题,如锁、竞争冲突,想要用好多线程不是那么容易。

线程之间上下文切换过程:

一个线程需要让出CPU执行时间,切换到另一个线程时,通常需要进行一次上下文切换,这包括保存当前线程的上下文(寄存器状态、堆栈等)并加载下一个线程的上下文。

大量的时间花费在保存和恢复寄存器和内存页表、更新内核相关数据结构等操作。

具体可以看:

https://baike.baidu.com/item/%E4%B8%8A%E4%B8%8B%E6%96%87%E5%88%87%E6%8D%A2

Golang调度原理-浅析_第1张图片

为了提高CPU利用率,减少多线程之间上下文切换花费时间,于是go设计出了协程。

golang实现的线程模型是两级线程模型(M:N)**

即: M个用户线程(协程)对应N个内核线程

线程模型共有三种:分别是一对一(1:1)模型多对一(N:1)模型多对多(M:N)模型

一对一(1:1)模型

1个协程绑定1个线程,协程的调度都由CPU完成,能利用多核。每个线程都有独立的寄存器集、堆栈和调用栈等资源,线程之间的调度和切换交给操作系统内核负责。如果这样做的话,干嘛不直接用多线程呢?

多对一(N:1)模型

多个协程被映射到一个操作系统线程上执行。协程直接切换又应用进程的调度器完成。但是这样做又用不了多核了。

多对多(M:N)模型

M个用户线程(协程)对应N个内核线程

既能让调度在用户空间完成,避免上下文切换。又能利用多核处理能力。可以这么理解:

有多个工人(协程)和多个工作台(线程)。每个工人可以在不同的工作台上完成不同的任务。一个工人可以负责原材料准备,另一个工人可以负责组装产品,还有一个工人可以负责包装。他们之间可以并行工作,互不打扰,如果有那个工作台空出来了,其他工人还可以去使用。这种模型可以更好地利用多核处理器的并行性,提高整体生产效率。

GMP模型

G: 协程(goroutine)

M: 线程(Machine),运行协程

P: 处理器(Processor),当P有任务时需要创建或者唤醒一个M来执行它队列里的任务,P拥有一个本地队列,存放这M。

刚才看了M:N模型,里面只有协程和线程的关系为何又多出来一个P?

从Golang 1.5版本开始,Golang引入了GMP模型,在1.5之前Golang使用的是GM模型+全局队列的方式。

全局队列存放多个协程,创建处理的协程首先会被加入到全局队列中,每次执行一个协程G的时候,内核线程M会从全局队列中获取一个协程G执行。多个线程刚好对应多个协程,刚好对应多个M:N关系。但是M从全局队列中读取协程的时候,需要加锁。频繁加锁解锁再高并发的时候就会代理一定性能问题。加锁解锁浪费时间,没有获取锁的M在等待中。

所以设计出一个P,在运行Go协程的过程中,P和M是1:1绑定关系。

M想要运行G的时候,首先从它绑定的P的本地队列中读取一个G来执行,然后再从全局队列中获取,如果从全局队列中没有获取到从其他P的队列偷取一部分P放到自己本地队列中。对比之前GM模型,M只能从全局队列获取,变成3种获取方式:从绑定的P本地队列中获取,从全局队列中获取,从其他P中偷取一半。这样做的目的就是减少大量的加锁解锁过程,提高效率。

从网上借个图:

Golang调度原理-浅析_第2张图片

P的数量:

  • 由启动时环境变量$GOMAXPROCS或者是由runtime的方法GOMAXPROCS()决定。假如P的数量等于CPU核心数,那么就是所有的核心都能运行协程。协程数量和任务多时,就能发挥出CPU最佳性能。PS:说一点,协程数量不是越多越好,GO协程调度小和内存占用小,不代表没有调度开销和内存开销。

M的数量:

M和P是1:1绑定,但是一个M阻塞后,P会和M解绑,P会从M的休眠队列中找一个空闲M或者重新创建一个M。M的总数量没有绝对关系,系统设置最大10000。

为什么M阻塞,P会和M解绑?

当线程阻塞时,它会释放CPU资源,当线程执行阻塞操作时,它会主动让出CPU,将执行权交给其他可运行的线程。如果解绑那么P对应那个CPU内核,在阻塞时,不能重复利用CPU资源

M阻塞完成后,会重新进入M队列休眠。

如果一个协程一直占用线程进行调度时,怎么办?

runtime.main中会创建一个额外m运行sysmon函数,

func sysmon() {
    lasttrace := int64(0)
    idle := 0 // how many cycles in succession we had not wokeup somebody
    delay := uint32(0)
    for {
        if idle == 0 { // start with 20us sleep...
            delay = 20
        } else if idle > 50 { // start doubling the sleep after 1ms...
            delay *= 2
        }
        if delay > 10*1000 { // up to 10ms
            delay = 10 * 1000
        }
        usleep(delay)

        ......
    }       
}

sysmon会进入一个无限循环, 第一轮回休眠20us, 之后每次休眠时间倍增, 最终每一轮都会休眠10ms,每10秒给M发一次消息,向正在运行的 goroutine 所绑定的的那个 M(也可以说是线程)发出 SIGURG 信号,抢占P。这个过程叫基于信号的抢占式调度。

你可能感兴趣的:(golang)