当前位置: 移动技术网 > IT编程>脚本编程>Go语言 > Go routine调度详解

Go routine调度详解

2019年05月28日  | 移动技术网IT编程  | 我要评论

goroutine简介

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

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

go routine的调度原理和操作系统的线层调度是比较相似的。这里我们将介绍go routine的相关知识。

goroutine(有人也称之为协程)本质上go的用户级线程的实现,这种用户级线程是运行在内核级线程之上。当我们在go程序中创建goroutine的时候,我们的这些routine将会被分配到不同的内核级线程中运行。一个内核级线程可能会负责多个routine的运行。而保证这些routine在内内核级线程安全、公平、高效运行的工作,就由调度器来实现。

goroutine内部原理

概念介绍

在进行实现原理之前,了解下一些关键性术语的概念。

并发

一个cpu上能同时执行多项任务,在很短时间内,cpu来回切换任务执行(在某段很短时间内执行程序a,然后又迅速得切换到程序b去执行),有时间上的重叠(宏观上是同时的,微观仍是顺序执行),这样看起来多个任务像是同时执行,这就是并发。

并行

当系统有多个cpu时,每个cpu同一时刻都运行任务,互不抢占自己所在的cpu资源,同时进行,称为并行。

进程

cpu在切换程序的时候,如果不保存上一个程序的状态(也就是我们常说的context--上下文),直接切换下一个程序,就会丢失上一个程序的一系列状态,于是引入了进程这个概念,用以划分好程序运行时所需要的资源。因此进程就是一个程序运行时候的所需要的基本资源单位(也可以说是程序运行的一个实体)。

线程

cpu切换多个进程的时候,会花费不少的时间,因为切换进程需要切换到内核态,而每次调度需要内核态都需要读取用户态的数据,进程一旦多起来,cpu调度会消耗一大堆资源,因此引入了线程的概念,线程本身几乎不占有资源,他们共享进程里的资源,内核调度起来不会那么像进程切换那么耗费资源。

协程

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

go调度的组成

go的调度主要有四个结构组成,分别是:

  • g:goroutine的核心结构,包括routine的栈、程序计数器pc、以及一些状态信息等;
  • m:内核级线程。goroutine在m上运行。m中信息包括:正在运行的goroutine、等待运行的routine列表等。当然也包括操作系统线程相关信息,这些此处不讨论。
  • p:processor,处理器,只要用于执行goroutine,维护了一个goroutine列表。其实p是可以从属于m的。当p从属于(分配给)m的时候,表示p中的某个goroutine得以运行。当p不从属于m的时候,表示p中的所有goroutine都需要等待被安排到内核级线程运行。
  • sched:调度器,存储、维护m,以及一个全局的goroutine等待队列,以及其他状态信息。

go程序的启动过程

  • 初始化sched:一个存储p的列表pidle。p的数量可以通过gomaxprocs设置;
  • 创建第一个goroutine。这个goroutine会创建一个m,这个内核级线程(sysmon)的工作是对goroutine进行监控。之后,这个goroutine开始我们在main函数里面的代码,此时,该goroutine就是我们说的主routine。

创建goroutine:

  • goroutine创建时指定了代码段
  • 然后,goroutine被加入到p中去等待运行。
  • 这个新建的goroutine的信息包含:栈地址、程序计数器

创建内核级线程m

内核级线程由go的运行时根据实际情况创建,我们无法再go中创建内核级线程。那什么时候回创建内核级线程呢?当前程序等待运行的goroutine数量达到一定数量及存在空闲(为被分配给m)的p的时候,go运行时就会创建一些m,然后将空闲的p分配给新建的内核级线程m,接着才是获取、运行goroutine。创建m的接口函数如下:

// 创建m的接口函数
void newm(void (*fn)(void), p *p)

// 分配p给m
if(m != &runtime·m0) {â
  acquirep(m->nextp);
  m->nextp = nil;
}
// 获取goroutine并开始运行
schedule();

m的运行

static void schedule(void)
{
  g *gp;

  gp = runqget(m->p);
  if(gp == nil)
    gp = findrunnable();

 // 如果p的类别不止一个goroutine,且调度器中有空闲的的p,就唤醒其他内核级线程m
  if (m->p->runqhead != m->p->runqtail &&
    runtime·atomicload(&runtime·sched.nmspinning) == 0 &&
    runtime·atomicload(&runtime·sched.npidle) > 0) // todo: fast atomic
    wakep();
 // 执行goroutine
  execute(gp);
}

  • runqget: 从p中获取goroutine即gp。gp可能为nil(如m刚创建时p为空;或者p的goroutine已经运行完了)。
  • findrunnable:寻找空闲的goroutine(从全局的goroutine等待队列获取goroutine;如果所有goroutine都已经被分配了,那么从其他m的p的goroutine的goroutine列表获取一些)。如果获取到goroutine,就将他放入p中,并执行它;否则没能获取到任何的goroutine,该内核级线程进行系统调用sleep了。
  • wakep:当当前内核级线程m的p中不止一个goroutine且调度器中有空闲的的p,就唤醒其他内核级线程m。(为了找些空闲的m帮自己分担)。

routine状态迁移

前面说的是g,m是怎样创建的以及什么时候创建、运行。那么goroutine在m是是怎样进行调度的呢?这个才是goroutine的调度核心问题,即上面代码中的schedule。在说调度之前,我们必须知道goroutine的状态有什么,以及各个状态之间的关系。

  • gidle:创建中的goroutine,实际上这个状态没有什么用;
  • grunnable:新创建完成的goroutine在完成了资源的分配及初始化后,会进入这个状态。这个新创建的goroutine会被分配到创建它的m的p中;
  • grunning:当grunnable中的goroutine等到了空闲的cpu或者到了自己的时间片的时候,就会进入grunning状态。这个装下的goroutine可以被前文提到的findrunnable函数获取;
  • gwaiting:当正在运行的goroutine进行一些阻塞调用的时候,就会从grunning状态进入gwaiting状态。常见的调用有:写入一个满的channel、读取空的channel、io操作、定时器ticker等。当阻塞调用完成后,goroutine的状态就会从gwaiting转变为grunnable;
  • gsyscall:当正在运行的goroutine进行系统调用的时候,其状态就会转变为gsyscall。当系统调用完成后goroutine的状态就会变为grunnable。(前文提到的sysmon进程会监控所有的p,如果发现有的p的系统调用是阻塞式的或者执行的时间过长,就会将p从原来的m分离出来,并新建一个m,将p分配给这个新建的m)。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持移动技术网。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网