goroutine调度器是建立在操作系统线程之上的,本文大概的介绍一下有关操作系统线程和线程调度规则的一些概念和含义。
现在抛开定义,根据如下一段C程序直观感受下什么是线程:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#define N (1000 * 1000 * 1000)
volatile int g = 0;
void *start(void *arg)
{
int i;
for (i = 0; i < N; i++) {
g++;
}
return NULL;
}
int main(int argc, char *argv[])
{
pthread_t tid;
// 使用pthread_create函数创建一个新线程执行start函数
pthread_create(&tid, NULL, start, NULL);
for (;;) {
usleep(1000 * 100 * 5);
printf("loop g: %d\n", g);
if (g == N) {
break;
}
}
pthread_join(tid, NULL); // 等待子线程结束运行
return 0;
}
C有常用的pthread线程库,其创建的用户态线程其实就是Linux操作系统内核所支持的线程,与Go工作线程是一样的,这些线程都有Linux内核负责调度和管理,Go在此之上做了一个goroutine,实现了一个二级线程。
上述程序运行之后会有两个线程,一是操作系统将程序加载起来运行时创建的主线程,另外一个是主线程调用pthread_create创建的start子线程,主线程在创建完子线程之后,每隔500毫秒打印一下全局变量g的值,直到g等于10亿,而start线程启动后就开始执行一个10亿次的对g自增加1的循环,这两个线程同时并发在系统中运行,操作系统内核负责对其进行调度和管理,编程人员无法精准预知某个线程何时运行。
关于操作系统对线程的调度,有以下两个问题:
-
何时发生调度?
-
调度时会做哪些事情?
操作系统必须在得到CPU控制权之后才能发生调度,那接下来的问题就是用户程序在CPU运行时如何让CPU去执行操作系统代码从而让内核获取控制权?
以下两种情况时,CPU会从执行用户程序跳转去执行操作系统代码:
-
用户程序使用系统调用进入操作系统内核。
-
硬件中断。硬件中断处理程序由操作系统提供,发生硬件中断会执行此代码。
硬件中断有个很重要的时钟中断,这是操作系统发起抢占调度的基础。
操作系统会在执行操作系统代码路径上某点检查是否需要调度,所以操作系统对线程的调度,也会发生在上述两种情况之中。
来看下在单核电脑上述C程序的输出:
bobo@ubuntu:~/study/c$ gcc thread.c -o thread -lpthread
bobo@ubuntu:~/study/c$ ./thread
loop g: 98938361
loop g: 198264794
loop g: 297862478
loop g: 396750048
loop g: 489684941
loop g: 584723988
loop g: 679293257
loop g: 777715939
loop g: 876083765
loop g: 974378774
loop g: 1000000000
可以看出,主线程和start在轮流运行,这就是操作系统对它们进行调度的结果,操作系统一会儿将start调度起来运行,一会儿将主线程调度起来运行。
从程序输出结果可以看到抢占调度的身影,因为主线程在start运行过程中得到了运行,而start线程执行start函数没有发生系统调用,并且这个程序又是在单核操作系统中运行,没有其它CPU来运行主线程,所以如果没有中断时发生抢占调度,操作系统就无法获取CPU的控制权,也就不可能发生线程调度。
再来看看操作系统在发送线程调度时会发生什么事儿?
操作系统会将不同的线程调度到同一个CPU上去执行,而每个线程运行时又会使用CPU的寄存器,但每个CPU只有一组寄存器,所以操作系统将线程B调度到CPU运行时需先将正在运行线程A所使用的的寄存器的值全部存储到内存,再将线程B保存在内存中的寄存器的值放回CPU寄存器中,如此B就恢复到之前运行的状态接着执行。
线程调度时操作系统需保存和恢复的寄存器除了通用寄存器之外,还有指令指针寄存器rip以及栈相关的栈顶寄存器rsp和栈基寄存器rbp,rip决定了线程下一条需要执行的指令,两个栈寄存器决定了线程执行时需要使用到的栈内存,所以恢复CPU寄存器的值就相当于改变了CPU下一条将要执行的指令,同时也切换了函数调用栈。
从调度器的角度来看,线程至少包含以下三个部分:
-
一组通用寄存器的值。
-
将要执行的下一条指令的地址。
-
栈。
到此,操作系统对线程的调度可以简单理解为内核调度器对不同线程所使用的寄存器和栈的切换。
来对操作系统线程下一个简单的定义:操作系统线程是由内核负责调度且拥有私有的一组寄存器的值和栈的执行流。
以上仅为个人观点,不一定准确,能帮到各位那是最好的。
好啦,到这里本文就结束了,喜欢的话就来个三连击吧。
扫码关注公众号,获取更多优质内容。