社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow
也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!
来源:https://www.douban.com/note/300631999/
goroutine是golang的一大特色,或者可以说是最大的特色吧(据我了解),这篇文章主要翻译自Morsing的[这篇博客](http://morsmachine.dk/go-scheduler),我读这篇文章的时候不只是赞叹调度器设计的精巧,而且被Unix内核设计思想的影响和辐射所震撼,感觉好多好东西都带着它的影子。
绪论(Introduction)
---------------------
Go 1.1最大的特色之一就是这个新的调度器,由Dmitry Vyukov贡献。新调度器让并行的Go程序获得了一个动态的性能增长,针对它我不能再做点更好的工作了,我觉得我还是为它写点什么吧。
这篇博客里面大多数东西都已经被包含在了[原始设计文档](https://docs.google.com/document/d/1TTj4T2JO42uD5ID9e89oa0sLKhJYD0Y_kqxDv3I3XMw)中了,这个文档的内容相当广泛,但是过于技术化了。
关于新调度器,你所需要知道的都在那个设计文档中,但是我这篇博客有图片,所以更加清晰易懂。
带调度器的Go runtime需要什么?(What does the Go runtime need with a scheduler?)
-------------------------------------------------------------------------------
但是在我们开始看新调度器之前,我们需要理解为什么需要调度器。为什么既然操作系统能为我们调度线程了,我们又创造了一个用户空间调度器?
POSIX线程API是对现有Unix进程模型的一个非常大的逻辑扩展,而且线程获得了非常多的跟进程相同的控制。比如,线程有它自己的信号掩码,线程能够被赋予CPU affinity功能(就是指定线程只能在某个CPU上运行),线程能被添加到[cgroups](http://en.wikipedia.org/wiki/Cgroup)中,线程所用到的资源也可以被查询到。所有的这些控制增大了Go程序使用gorroutines时根本不需要的特性(features)的开销,当你的程序有100,000个线程的时候,这些开销会急剧增长。
另外一个问题是,基于Go模型,操作系统不能给出特别好的决策。比如,当运行一次垃圾收集的时候,Go的垃圾收集器要求所有线程都被停止而且要求内存要处于一致状态(consistent state)。这个涉及到要等待全部运行时线程(running threads)到达一个点(point),我们事先知道在这个地方内存是一致的。
当很多被调度的线程分散在随机的点(random point)上的时候,结果就是你不得不等待他们中的大多数到达一致状态。Go调度器能够作出这样的决策,就是只在内存保持一致的点上进行调度。这就意味着,当我们为垃圾收集而停止的时候,我们只须等待在一个CPU核(CPU core)上处于活跃运行状态的线程即可。
来看看里面的各个角色(Our Cast of Characters)
-----------------------------------------
目前有三个常见的线程模型。一个是N:1的,即多个用户空间线程运行在一个OS线程上。这个模型可以很快的进行上下文切换,但是不能利用多核系统(multi-core systems)的优势。另一个模型是1:1的,即可执行程序的一个线程匹配一个OS线程。这个模型能够利用机器上的所有核心的优势,但是上下文切换非常慢,因为它不得不陷入OS(trap through the OS)。
Go试图通过M:N的调度器去获取这两个世界的全部优势。它在任意数目的OS线程上调用任意数目的goroutines。你可以快速进行上下文切换,并且还能利用你系统上所有的核心的优势。这个模型主要的缺点是它增加了调度器的复杂性。
为了完成调度任务,Go调度器使用了三个实体:
总体介绍
$GOROOT/src/pkg/runtime目录很重要,值得好好研究,源代码可以从runtime.h开始读起。
proc.c中是实现的线程调度相关。
goroutine实现的是自己的一套线程系统,语言级的支持,与pthread或系统级的线程无关。
一些重要的结构体定义在runtime.h中。两个重要的结构体是G和M
结构体G名字应该是goroutine的缩写,相当于操作系统中的进程控制块,在这里就是线程的控制结构,是对线程的抽象。
其中包括:
goid //线程ID
status//线程状态,如Gidle,Grunnable,Grunning,Gsyscall,Gwaiting,Gdead等
有个常驻的寄存器extern register G* g被使用,这个是当前线程的线程控制块指针。amd64中这个寄存器是使用R15,在x86中使用0(GS) 分段寄存器
结构体M名字是machine的缩写。是对机器的抽象,其实是对应到操作系统线程。
goroutine的生老病死
go关键字最终被弄成了runtime.newproc.就以这个为出发点看整个调度器吧
runtime.newproc功能是创建一个新的g.这个函数不能用分段栈,真正的工作是调用newproc1完成的.newproc1的动作包括:
分配一个g的结构体
初始化这个结构体的一些域
将g挂在就绪队列
引发一次matchmg
初始化newg的域时,会将调用参数保存到g的栈;将sp,pc等上下文环境保存在g的sched域.这样当这个g被分配了一个m时就可以运行了.
接下来看matchmg函数.这个函数就是做个匹配,只要m没有突破上限GOMAXPROCS,就拿一个m绑定一个g.
如果m的waiting队列中有就从队列中拿,否则就要新建一个m,调用runtime.newm
runtime.newm功能跟newproc相似,前者分配一个goroutine,而后者是分配一个machine.调用的runtime.newosproc函数.
其实一个machine就是一个操作系统线程的抽象,可以看到它会调用runtime.newosproc.
这个新线程会以mstart作为入口地址.当m和g绑定后,mstart会恢复g的sched域中保存的上下文环境,然后继续运行.
随便扫一下runtime.newosproc还是蛮有意思的,代码在thread_linux.c文件中(平台相关的),它调用了runtime.clone(平台相关). runtime.clone是用汇编实现的,代码在sys_linux_386.s.可以看到上面有
INT $0x80
看到这个就放心了,只要有一点汇编基础,你懂的.可以看出,go的runtime果然跟c的runtime半毛钱关系都没有啊
回到runtime.newm函数继续看,它调用runtime.newosproc建立了新的线程,线程是以runtime.mstart为入口的,那么接下来看mstart函数.
mstart是runtime.newosproc新建的线程的入口地址,新线程执行时会从这里开始运行.
新线程的执行和goroutine的执行是两个概念,由于有m这一层对机器的抽象,是m在执行g而不是线程在执行g.所以线程的入口是mstart,g的执行要到schedule才算入口.函数mstart的最后调用了schedule.
终于到了schedule了!
如果从mstart进入到schedule的,那么schedule中逻辑非常简单,前面省了一大段代码.大概就这几步:
找到一个等待运行的g
将它搬到m->curg,设置好状态为Grunning
直接切换到g的上下文环境,恢复g的执行
goroutine从newproc出生一直到运行的过程分析,到此结束!
虽然按这样a调用b,b调用c,c调用d,d调用e的方式去分析源代码谁看都会晕掉,但我还是想重复一遍这里的读代码过程后再往下写些有意思的,希望真正感兴趣的读者可以拿着注释过的源码按顺序走一遍:
newproc -> newproc1 -> newprocreadylocked -> matchmg -> (可能引发)newm -> newosproc -> (线程入口)mstart -> schedule -> gogo跳到goroutine运行
以上状态变化经历了Gwaiting->Grunnable->Grunning,经历了创建,到挂在就绪队列,到从就绪队列拿出并运行.下面将从其它几种状态变化继续看调度器,从runtime.entersyscall开始.
runtime.entersyscall做的事情大致是设置g的状态为Gsyscall,减少mcpu.
如果mcpu减少之后小于mcpumax了并且有处于就绪态的g,则matchmg
runtime.exitsyscall函数中,如果退出系统调用后mcpu小于mcpumax,直接设置g的状态Grunning.表示让它继续运行.
否则如果mcpu达到上限了,则设置readyonstop,表示下一次schedule中将它改成Grunnable了放到就绪队列中
现在Gwaiting,Grunnable,Grunning,Gwaiting都出现过的,接下来看最后两种状态Gmoribund和Gdead.
看runtime.goexit函数.这个函数直接把g的状态设置成Gmoribund,然后调用gosched,进入到schedule中.
在schedule中如果遇到状态为Gmoribund的g,直接设置g的状态为Gdead,将g与m分离,把g放回到free队列.
简单理解
接下来看一些有意思点的吧,先不读代码了.一个常规的 线程池+任务队列 的模型如图所示:
把每个工作线程叫worker的话,每条线程运行一个worker,每个worker做的事情就是不停地从队列中取出任务并执行:
while (!empty(queue)) { |
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!