设为首页 加入收藏

TOP

深度解密Go语言之 scheduler(一)
2019-09-04 00:56:26 】 浏览:162
Tags:深度 解密 语言 scheduler

好久不见,你还好吗?距离上一篇文章已经过去了一个多月了,迟迟未更新文章,我也很着急啊。

跟大家汇报一下,这段时间我在看 proc.go 的源码,其实就是调度器的源码。代码有几千行之多,不像以往的 map,channel 等等。想把这些代码都看明白,是一个庞大的工程。到今天为止,我也不敢说我都看明白了。

要深挖下去的话,会无穷无尽,所以阶段性的探索就到这里。接下来就把这段时间的探索分享出来。

其实,今天这篇文章仅仅算是一个引子,接下来会连续发布十篇系列文章。目录如下:

系列文章目录

而这个系列的文章主要是受公众号“go 语言核心编程技术”的启发,它有一个 Go 调度器的系列教程,写得非常赞,强烈推荐大家去看,后面会经常引用到它的文章。我忍不住在这贴上公众号的二维码,一定要去关注啊。这是我在找资料的过程中发现的一个宝藏,本来想私藏着,但是好东西还是要分享给大家,不能固步自封。

Go 语言核心编程技术

开始我们今天的正题。

一个月前,《Go 语言高级编程》作者柴树杉老师在 CSDN 上发表了一篇《Go 语言十年而立,Go2 蓄势待发》,视角十分宏大。我们既要低头看路,有时也要抬头看天,这篇文章就属于“抬头”看天类的,推荐阅读。

文章中提到了第一本写 Go 的小说《胡文 Go》。我找来看了下,嬉笑怒骂,还挺有意思的。书中有这样一句话:

在 Go 语言里,go func 是并发的单元,chan 是协调并发单元的机制,panic 和 recover 是出错处理的机制,而 defer 是神来之笔,大大简化了出错的管理。

Goroutines 在同一个用户空间里同时独立执行 functions,channels 则用于 goroutines 间的通信和同步访问控制。

上一篇文章里我们讲了 channel,并且提到,goroutine 和 channel 是 Go 并发编程的两大基石,那这篇文章就聚焦到 goroutine,以及调度 goroutine 的 go scheduler。

前置知识

os scheduler

从操作系统角度看,我们写的程序最终都会转换成一系列的机器指令,机器只要按顺序执行完所有的指令就算完成了任务。完成“按顺序执行指令”任务的实体就是线程,也就是说,线程是 CPU 调度的实体,线程是真正在 CPU 上执行指令的实体。

每个程序启动的时候,都会创建一个初始进程,并且启动一个线程。而线程可以去创建更多的线程,这些线程可以独立地执行,CPU 在这一层进行调度,而非进程。

OS scheduler 保证如果有可以执行的线程时,就不会让 CPU 闲着。并且它还要保证,所有可执行的线程都看起来在同时执行。另外,OS scheduler 在保证高优先级的线程执行机会大于低优先级线程的同时,不能让低优先级的线程始终得不到执行的机会。OS scheduler 还需要做到迅速决策,以降低延时。

线程切换

OS scheduler 调度线程的依据就是它的状态,线程有三种状态(简化模型):Waiting, Runnable or Executing

状态 解释
Waiting 等待状态。线程在等待某件事的发生。例如等待网络数据、硬盘;调用操作系统 API;等待内存同步访问条件 ready,如 atomic, mutexes
Runnable 就绪状态。只要给 CPU 资源我就能运行
Executing 运行状态。线程在执行指令,这是我们想要的

线程能做的事一般分为两种:计算型、IO 型。

计算型主要是占用 CPU 资源,一直在做计算任务,例如对一个大数做质数分解。这种类型的任务不会让线程跳到 Waiting 状态。

IO 型则是要获取外界资源,例如通过网络、系统调用等方式。内存同步访问控制原语:mutexes 也可以看作这种类型。共同特点是需要等待外界资源就绪。IO 型的任务会让线程跳到 Waiting 状态。

线程切换就是操作系统用一个处于 Runnable 的线程将 CPU 上正在运行的处于 Executing 状态的线程换下来的过程。新上场的线程会变成 Executing 状态,而下场的线程则可能变成 Waiting 或 Runnable 状态。正在做计算型任务的线程,会变成 Runnable 状态;正在做 IO 型任务的线程,则会变成 Waiting 状态。

因此,计算密集型任务和 IO 密集型任务对线程切换的“态度”是不一样的。由于计算型密集型任务一直都有任务要做,或者说它一直有指令要执行,线程切换的过程会让它停掉当前的任务,损失非常大。

相反,专注于 IO 密集型的任务的线程,如果它因为某个操作而跳到 Waiting 状态,那么把它从 CPU 上换下,对它而言是没有影响的。而且,新换上来的线程可以继续利用 CPU 完成任务。从整个操作系统来看,“工作进度”是往前的。

记住,对于 OS scheduler 来说,最重要的是不要让一个 CPU 核心闲着,尽量让每个 CPU 核心都有任务可做。

If you have a program that is focused on IO-Bound work, then context switches are going to be an advantage. Once a Thread moves into a Waiting state, another Thread in a Runnable state is there to take its place. This allows the core to always be doing work. This is one of the most important aspects of scheduling. Don’t allow a core to go idle if there is work (Threads in a Runnable state) to be done.

函数调用过程分析

要想理解 Go scheduler 的底层原理,对于函数调用过程的理解是必不可少的。它涉及到函数参数的传递,CPU 的指令跳转,函数返回值的传递等等。这需要对汇编语言有一定的了解,因为只有汇编语言才能进行像寄存器赋值这样的底层操作。之前的一些文章里也有说明,这里再来复习一遍。

函数栈帧的空间主要由函数参数和返回值、局部变量和被调用其它函数的参数和返回值空间组成。

宏观看一下,Go 语言中函数调用的规范,引用曹大博客里的一张图:

曹大 asmshare 函数调用规范

Go plan9 汇编通过栈传递函数参数和返回值。

调用子函数时,先将参数在栈顶准备好,再执行 CALL 指令。CALL 指令会将 IP 寄存器的值压栈,这个值就是函数调用完成后即将执行的下一条指令。

然后,就会进入被调用者的栈帧。首先会将 caller BP 压栈,这表示栈基址,也就是栈底。栈顶和栈基址定义函数的栈帧。

CALL 指令类似 PUSH IP 和 JMP somefunc 两个指令的组合,首先将当前的 IP 指令寄存器的值压入栈中,然后通过 JMP 指令将要调用函数的地址写入到 IP 寄存器实现跳转。

而 RET 指令则是和 CALL 相反的操作,基本和 POP IP 指令等价

首页 上一页 1 2 3 4 5 下一页 尾页 1/5/5
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇golang微服务框架go-micro 入门笔.. 下一篇Golang检测Linux服务器端口占用

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目