外星人为什么还没降落到地球上?

这周五午间休息时无意中看了36kr发布的一篇文章:《开电脑与外星人尬聊?搜寻地外文明项目SETI@home的极客简史》,这是一篇译文,原文发表在《大西洋月刊》,题为“A Brief History of SETI@Home”,文章科普了SETI@HOME这一项目的简史。

SETI是“Search for Extraterrestrial Intelligence”,字面意思就是搜寻外星的智慧生命体。这个项目从技术本质上看就是一个超级庞大的分布式系统,它利用加入“志愿计算”计划的志愿者的PC上的空闲计算能力来分析射电望远镜观测到的信号,旨在从这些信号中过滤并识别出可能的外星生命发出的信号。SETI@home项目于1999年就启动了,不过令人感到尴尬的是至今这个项目仍然是一无所获:外星人的毛儿都没找到。虽然结果不如预期,但这不能否定这一项目的伟大。国内的地外文明爱好者、ET/alien爱好者们,如果你想贡献出自己的一份薄力,就去SETI@Home项目官网下载一个软件,加入到这个庞大的计算网络中吧。

不过这里并不想和大家探讨SETI@Home的技术细节,而是要考量另外一个问题:外星人为什么还没降落到地球上?

1、人类探索宇宙的历程简述

在探索地外生命的道路上,人类不可谓不努力,探索的时间也不可谓不长。从已知的、公认的人类探索宇宙的大事件资料中,我们知道:

  • 公元前500-400年,中国人就开始制作木鸟并试验原始飞行器;
  • 起源于古代中国的风筝于约公园14世纪传到欧洲;
  • 1903年12月14日至17日,由莱特兄弟设计制造的“飞行者”1号飞机,在人类航空史上首次实现了自主操纵飞行。从此人类进入航空新纪元;
  • 1909年世界第一架轻型飞机在法国诞生;
  • 1947年10月14日美国著名试飞员查尔斯·耶格尔驾驶X-1飞机实现了突破音障飞行;
  • 1957年10月4日,前苏联发射世界第一颗人造地球卫星。半年后,美国的人造卫星上天;从此人类进入近地轨道探索的时代;
  • 1969年7月20日22时56分20秒,美国宇航员阿姆斯特朗乘坐“阿波罗”11号飞船成功登陆月球,成为人类踏上月球的第一人,也是人类踏上地外星球的第一人,从此人类开启了太阳系内探索新时代;
  • 1970年12月15日,前苏联“金星”7号探测器首次在金星上着陆;
  • 1971年12月2日,前苏联“火星”3号探测器在火星表面着陆。5年后,美国的“海盗”火星探测器登陆火星;
    … …

人类在探索近地轨道和太阳系内行星的同时,不忘太阳系外的深层空间的探索。不过限于人类在技术方面的局限,人类文明仅仅能将少量信息带出太阳系。这其中尤为值得注目的是美国的先驱者系列和旅行者系列深空探测器的发射,它们分别携带了人类送给地外文明的“礼物”:

img{512x368}

两艘“先驱者号”各携带一张相同的镀金铝板(见上图),长22.9厘米,宽15.2厘米,其上刻有一男一女的画像,那位男人正在招手致意。还有象征太阳系的信息,以及一些表示这艘星际飞船来历的符号。

img{512x368}

两艘“旅行者号”各携带一块相同的镀金唱片(见上图)和一枚金刚石唱针。唱片名为《地球之音》,直径有22.9厘米,上面录制了人类向外星文明发出的55种问候语(包括中国的现代标准汉语、闽南方言、粤语和
吴语)、长达90分钟的27首各国著名乐曲(包括中国传统名曲《高山流水》等)录音,还有115幅地球上各种事物和情景的图片。这两张唱片可以在宇宙中保存10亿年之久。

目前四个飞行器均已飞出了太阳系,成为茫茫宇宙中流浪的四位地球信使。

除了发射探测器,人类还在不断加大投入,增强自己的观察能力- 建造“地球的眼睛”。

  • 1990年4月25日,美国航空航天局NASA使用航天飞机将哈勃太空望远镜送入预定轨道,从此人类将在近地轨道拥有了自己看宇宙的“眼睛”。由于没有大气湍流的干扰,它所获得的图像和光谱具有极高的稳定性和可重复性,其清晰度是地面天文望远镜的10倍以上。

img{512x368}
哈勃望远镜

  • 2016年9月25日,位于中国贵州黔南州平塘县大窝凼的世界最大单口径射电望远镜——500米口径球面射电望远镜(FAST)全部竣工并投入使用。

img{512x368}
FAST:世界最大单口径射电望远镜

不过,虽然发射出去如此多的探测器,建造了如此先进的“千里眼”,尴尬的是人类文明到目前为止依然没有得到地外文明的任何蛛丝马迹。我们不禁再次发问:外星人究竟在哪?为何还没降落到地球?

对于这样一个问题,从技术角度来说,人类肯定无法给出答案。但这并不影响我们去管窥到这个问题答案的一角,因为我们还有想象力和逻辑推理能力。

2、关于“文明”

“文明” ,对应的英文单词是civilization,英英释义是“the way people live together in a certain place at a certain time“,中文字面意思的理解就是一群人在一定的时间、在一定的地点生活在一起的方式,这是一个极具弹性的定义。时间、地点、人群数量的跨度都是可大可小、可长可短的。

按照这个定义,我们可以简单粗暴地将文明分类为:局部文明(比如:中国文明、西方文明、玛雅文明等)、星球文明(比如:地球文明)、星系文明(比如:银河系文明)、宇宙文明….。之所以给出这个分类,是因为我们采用“以己为鉴、以史为镜”的方法来推测地外文明。

谈到人类对宇宙中文明形态、行为的推测,我们不得不提到中国科幻大师刘慈欣在其巨著《三体》系列中提出的“宇宙社会学”的概念和大胆推测:

基本公理:
一、生存是文明的第一需要。
二、文明不断增长和扩张,但宇宙中的物质总量保持不变。

猜疑链:
一个文明无法判断另一个文明对自己是善意或恶意的;
一个文明无法判断另一个文明认为自己是善意或恶意的;
一个文明无法判断另一个文明判断自己对她是善意或恶意的;
一个文明不能判断另一个文明是善文明还是恶文明;
一个文明不能判断另一个文明是否会对本文明发起攻击。

黑暗森林法则:宇宙就是一座黑暗森林,每个文明都是带枪的猎人,像幽灵般潜行与林间,轻轻拨开挡路的树枝,竭力不让脚步发出一点儿声音,连呼吸都必须小心翼翼......他必须小心,因为林中到处都有与他一样
潜行的猎人,如果他发现了别的生命,不管是不是猎人,不管是天使还是魔鬼,不管是娇嫩的婴儿还是步履蹒跚的老人,也不管是天仙般的少女还是天神般的男孩,能做的只有一件事:开枪消灭之?在
这片森林中,他人就是地狱,就是永恒的威胁,任何暴露自己存在的生命都将很快被消灭,这就是宇宙文明的图景,这就是对费米悖论的解释。

自己时常思考这样的一个问题,人类发展的终极目标是什么?从古代农耕布织、到18世纪的以蒸汽机器代替手工的第一次工业革命、从19世纪中后的以电气时代为特征的第二次工业革命、到20世纪的以核能、计算机为特征的第三次工业革命、再到近来开启的以互联网产业、人工智能为代表的“第四次工业革命”,人类都在追求生产力提升、生产效率的提升。当这种生产力提升到一个极大值之后,人类的下一个目标是什么呢?很多人会说,飞出地球,在其他星球建立殖民地,但这种行为背后的目的又是什么呢?地球文明的延伸?从根本上来说,这也是为了地球文明的存续,不管这是主动的(比如:为了商业目的的、政治和军事目的的)还是被动的(比如说:当地球环境将在未来一段时间后不再适合人类生存)。这基本符合大刘提到的基本公理第一条,但这不仅是第一需要,更是终极目标

在明确了自己文明的终极目标之后,在对外其他文明这个问题上,大刘给出了一个“黑暗”的推理 – 黑暗森林法则。关于这个法则,不得不说大刘的超一流的想象力,但同样是针对这一法则,争论也是存在的。”黑暗森林”法则的一个不可忽视的前提:“黑暗”。大刘直接给出设定:宇宙就是一座黑暗森林。这种设定本身就是“黑暗”的!难道文明存续的必须以毫无顾忌的消灭其他文明作为手段么?真的没有一个善良的文明么?真的不存在“光明森林”么?宇宙这么大,真不见得。如果人类拥有了毁灭一个星系的能力、并且观测到一个存在文明的行星,人类会毫不犹豫地直接将其毁灭吗?至少我们这个文明的大多数人不会赞同这么做。

3、关于文明间的“交互”

如果我们抛弃“黑暗森林”这一设定,我们生活在一个“光明森林”的宇宙中,那么“文明”间又是如何“交互(我也不是很确定,使用交互这个词是否准确)”的呢?

我们假设有两个独立发展的文明A和B,我们简单的为这两种文明定义了两种能力:观察能力和到达能力。在两个文明真正交互之前,文明的发展可能是这样的:

img{512x368}

  • a) 文明独立进化

初始阶段,两个文明独立进化,就像人类文明一样,可能从低级生命进化为高级生命,学会工具使用,智能提升,生产力提升,形成持续的、稳定的繁衍能力,技术水平也在不断提升。但这个阶段尚未具备宇宙观察能力,更没有通过观察确认文明存在的能力,尚未发展出航天技术。

  • b) 文明拥有初步观察能力和到达能力

随着两个文明的进化,各自发展出了对宇宙的观察能力和一定范围的到达能力。但这些能力有限,还不足以发现和确认对方文明的存在。

  • c) 一个文明具备了观察并确定对方文明存在的能力

一个文明A出现了技术爆炸,观察能力大幅提升,观察到并确认了B文明的存在。但此时A文明尚不具备到达B文明的能力。

  • d) 一个文明具备了到达对方文明的能力

A文明的技术爆炸继续,并发展出了到达B文明的能力。

接下来A文明会怎么做?这是需要重点探讨的。在大刘的“黑暗森林”下,三体星人出动了“质子”抑制地球科技发展、派出三体舰队意图灭绝人类;”歌者”更为直接地祭出了“二向箔”降维攻击武器毁灭了太阳系。但如果是在“光明森林”中呢?我想大刘的“猜疑链”依旧有效。A文明无法判断B文明的善与恶。于是A可能做出以下几个动作:

  • 远距离观察:虽然具备了到达能力,但A文明可能继续原则远距离观察B文明的先进程度,如果这是可以的话;

  • 近距离观察:如果A文明能基本确认B文明的技术水平远低于自己,A文明可能会派出“观察者”,到达B文明,并以一种“隐身”的方式近距离观察B文明,就像《星际迷航》中企业号所做的那样。

  • 接触:在确认了B文明足够善意的基础上,无论出于什么目的,A文明可能会主动接触B文明了。但必须肯定的是这种接触是十分危险的,即便这种接触的初衷是善意的。回顾人类内部的不同局部文明间的接触史,我们可以推测出这种接触很大可能是需要付出血的代价的 ,战争也许不可避免(我们只能以人类文明内部的接触作为参照物,否则还能怎么样呢)。

  • 融合:融合可能会发生,虽然可能是以一方文明付出惨重损失为代价的。但文明的存续的目的得以实现。

4、结论

地球文明目前尚处于具有一定观察能力和一定到达能力的阶段,非常初级。如果按照上面的文明发展阶段,应该处于b)阶段早期。地球文明的观察能力有限,关键是无法确认文明存在,到达能力也更是很有限的。好了,现在我们来分析文章开头提出的问题:外星文明为何没有降落到地球?有了我们之前的铺垫,针对这个问题,我们可以有几种可能的答案:

  • 不存在另外一个文明。从个人情感出发希望不存在这种情况,否则我们真成为宇宙中的孤独文明了;
  • 其他文明都和我们文明的发展阶段差不多,无法观察到我们,更无法到达我们;
  • 某个或某些高等文明在远距离观察我们或近距离以“隐身”的方式观察我们,尚在评估是否与我们接触。

从我们还存在这一事实,似乎证明宇宙并不“黑暗”,否则地球这么折腾(向深空发射飞行器等),也许早就被灭了。但如果宇宙法则真的是遵循大刘的“黑暗森林”法则,那么我们应该庆幸我们还没有被高等文明所发现。人类应该做的,唯有韬光养晦,努力发展科技,争取早日进入“技术爆炸”阶段,这才是存续地球文明的基础。

也谈goroutine调度器

Go语言在2016年再次拿下TIBOE年度编程语言称号,这充分证明了Go语言这几年在全世界范围内的受欢迎程度。如果要对世界范围内的gopher发起一次“你究竟喜欢Go的哪一点”的调查,我相信很多Gopher会提到:goroutine

GoroutineGo语言原生支持并发的具体实现,你的Go代码都无一例外地跑在goroutine中。你可以启动许多甚至成千上万的goroutine,Go的runtime负责对goroutine进行管理。所谓的管理就是“调度”,粗糙地说调度就是决定何时哪个goroutine将获得资源开始执行、哪个goroutine应该停止执行让出资源、哪个goroutine应该被唤醒恢复执行等。goroutine的调度是Go team care的事情,大多数gopher们无需关心。但个人觉得适当了解一下Goroutine的调度模型和原理,对于编写出更好的go代码是大有裨益的。因此,在这篇文章中,我将和大家一起来探究一下goroutine调度器的演化以及模型/原理。

注意:这里要写的并不是对goroutine调度器的源码分析,国内的雨痕老师在其《Go语言学习笔记》一书的下卷“源码剖析”中已经对Go 1.5.1的scheduler实现做了细致且高质量的源码分析了,对Go scheduler的实现特别感兴趣的gopher可以移步到这本书中去^0^。这里关于goroutine scheduler的介绍主要是参考了Go team有关scheduler的各种design doc、国外Gopher发表的有关scheduler的资料,当然雨痕老师的书也给我了很多的启示。

一、Goroutine调度器

提到“调度”,我们首先想到的就是操作系统对进程、线程的调度。操作系统调度器会将系统中的多个线程按照一定算法调度到物理CPU上去运行。传统的编程语言比如CC++等的并发实现实际上就是基于操作系统调度的,即程序负责创建线程(一般通过pthread等lib调用实现),操作系统负责调度。这种传统支持并发的方式有诸多不足:

  • 复杂

    • 创建容易,退出难:做过C/C++ Programming的童鞋都知道,创建一个thread(比如利用pthread)虽然参数也不少,但好歹可以接受。但一旦涉及到thread的退出,就要考虑thread是detached,还是需要parent thread去join?是否需要在thread中设置cancel point,以保证join时能顺利退出?
    • 并发单元间通信困难,易错:多个thread之间的通信虽然有多种机制可选,但用起来是相当复杂;并且一旦涉及到shared memory,就会用到各种lock,死锁便成为家常便饭;
    • thread stack size的设定:是使用默认的,还是设置的大一些,或者小一些呢?
  • 难于scaling

    • 一个thread的代价已经比进程小了很多了,但我们依然不能大量创建thread,因为除了每个thread占用的资源不小之外,操作系统调度切换thread的代价也不小;
    • 对于很多网络服务程序,由于不能大量创建thread,就要在少量thread里做网络多路复用,即:使用epoll/kqueue/IoCompletionPort这套机制,即便有libevent/libev这样的第三方库帮忙,写起这样的程序也是很不易的,存在大量callback,给程序员带来不小的心智负担。

为此,Go采用了用户层轻量级thread或者说是类coroutine的概念来解决这些问题,Go将之称为”goroutine“。goroutine占用的资源非常小(Go 1.4将每个goroutine stack的size默认设置为2k),goroutine调度的切换也不用陷入(trap)操作系统内核层完成,代价很低。因此,一个Go程序中可以创建成千上万个并发的goroutine。所有的Go代码都在goroutine中执行,哪怕是go的runtime也不例外。将这些goroutines按照一定算法放到“CPU”上执行的程序就称为goroutine调度器goroutine scheduler

不过,一个Go程序对于操作系统来说只是一个用户层程序,对于操作系统而言,它的眼中只有thread,它甚至不知道有什么叫Goroutine的东西的存在。goroutine的调度全要靠Go自己完成,实现Go程序内goroutine之间“公平”的竞争“CPU”资源,这个任务就落到了Go runtime头上,要知道在一个Go程序中,除了用户代码,剩下的就是go runtime了。

于是Goroutine的调度问题就演变为go runtime如何将程序内的众多goroutine按照一定算法调度到“CPU”资源上运行了。在操作系统层面,Thread竞争的“CPU”资源是真实的物理CPU,但在Go程序层面,各个Goroutine要竞争的”CPU”资源是什么呢?Go程序是用户层程序,它本身整体是运行在一个或多个操作系统线程上的,因此goroutine们要竞争的所谓“CPU”资源就是操作系统线程。这样Go scheduler的任务就明确了:将goroutines按照一定算法放到不同的操作系统线程中去执行。这种在语言层面自带调度器的,我们称之为原生支持并发

二、Go调度器模型与演化过程

1、G-M模型

2012年3月28日,Go 1.0正式发布。在这个版本中,Go team实现了一个简单的调度器。在这个调度器中,每个goroutine对应于runtime中的一个抽象结构:G,而os thread作为“物理CPU”的存在而被抽象为一个结构:M(machine)。这个结构虽然简单,但是却存在着许多问题。前Intel blackbelt工程师、现Google工程师Dmitry Vyukov在其《Scalable Go Scheduler Design》一文中指出了G-M模型的一个重要不足: 限制了Go并发程序的伸缩性,尤其是对那些有高吞吐或并行计算需求的服务程序。主要体现在如下几个方面:

  • 单一全局互斥锁(Sched.Lock)和集中状态存储的存在导致所有goroutine相关操作,比如:创建、重新调度等都要上锁;
  • goroutine传递问题:M经常在M之间传递”可运行”的goroutine,这导致调度延迟增大以及额外的性能损耗;
  • 每个M做内存缓存,导致内存占用过高,数据局部性较差;
  • 由于syscall调用而形成的剧烈的worker thread阻塞和解除阻塞,导致额外的性能损耗。

2、G-P-M模型

于是Dmitry Vyukov亲自操刀改进Go scheduler,在Go 1.1中实现了G-P-M调度模型work stealing算法,这个模型一直沿用至今:

img{512x368}

有名人曾说过:“计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”,我觉得Dmitry Vyukov的G-P-M模型恰是这一理论的践行者。Dmitry Vyukov通过向G-M模型中增加了一个P,实现了Go scheduler的scalable。

P是一个“逻辑Proccessor”,每个G要想真正运行起来,首先需要被分配一个P(进入到P的local runq中,这里暂忽略global runq那个环节)。对于G来说,P就是运行它的“CPU”,可以说:G的眼里只有P。但从Go scheduler视角来看,真正的“CPU”是M,只有将P和M绑定才能让P的runq中G得以真实运行起来。这样的P与M的关系,就好比Linux操作系统调度层面用户线程(user thread)与核心线程(kernel thread)的对应关系那样(N x M)。

3、抢占式调度

G-P-M模型的实现算是Go scheduler的一大进步,但Scheduler仍然有一个头疼的问题,那就是不支持抢占式调度,导致一旦某个G中出现死循环或永久循环的代码逻辑,那么G将永久占用分配给它的P和M,位于同一个P中的其他G将得不到调度,出现“饿死”的情况。更为严重的是,当只有一个P时(GOMAXPROCS=1)时,整个Go程序中的其他G都将“饿死”。于是Dmitry Vyukov又提出了《Go Preemptive Scheduler Design》并在Go 1.2中实现了“抢占式”调度。

这个抢占式调度的原理则是在每个函数或方法的入口,加上一段额外的代码,让runtime有机会检查是否需要执行抢占调度。这种解决方案只能说局部解决了“饿死”问题,对于没有函数调用,纯算法循环计算的G,scheduler依然无法抢占。

4、NUMA调度模型

从Go 1.2以后,Go似乎将重点放在了对GC的低延迟的优化上了,对scheduler的优化和改进似乎不那么热心了,只是伴随着GC的改进而作了些小的改动。Dmitry Vyukov在2014年9月提出了一个新的proposal design doc:《NUMA‐aware scheduler for Go》,作为未来Go scheduler演进方向的一个提议,不过至今似乎这个proposal也没有列入开发计划。

5、其他优化

Go runtime已经实现了netpoller,这使得即便G发起网络I/O操作也不会导致M被阻塞(仅阻塞G),从而不会导致大量M被创建出来。但是对于regular file的I/O操作一旦阻塞,那么M将进入sleep状态,等待I/O返回后被唤醒;这种情况下P将与sleep的M分离,再选择一个idle的M。如果此时没有idle的M,则会新创建一个M,这就是为何大量I/O操作导致大量Thread被创建的原因。

Ian Lance TaylorGo 1.9 dev周期中增加了一个Poller for os package的功能,这个功能可以像netpoller那样,在G操作支持pollable的fd时,仅阻塞G,而不阻塞M。不过该功能依然不能对regular file有效,regular file不是pollable的。不过,对于scheduler而言,这也算是一个进步了。

三、Go调度器原理的进一步理解

1、G、P、M

关于G、P、M的定义,大家可以参见$GOROOT/src/runtime/runtime2.go这个源文件。这三个struct都是大块儿头,每个struct定义都包含十几个甚至二、三十个字段。像scheduler这样的核心代码向来很复杂,考虑的因素也非常多,代码“耦合”成一坨。不过从复杂的代码中,我们依然可以看出来G、P、M的各自大致用途(当然雨痕老师的源码分析功不可没),这里简要说明一下:

  • G: 表示goroutine,存储了goroutine的执行stack信息、goroutine状态以及goroutine的任务函数等;另外G对象是可以重用的。
  • P: 表示逻辑processor,P的数量决定了系统内最大可并行的G的数量(前提:系统的物理cpu核数>=P的数量);P的最大作用还是其拥有的各种G对象队列、链表、一些cache和状态。
  • M: M代表着真正的执行计算资源。在绑定有效的p后,进入schedule循环;而schedule循环的机制大致是从各种队列、p的本地队列中获取G,切换到G的执行栈上并执行G的函数,调用goexit做清理工作并回到m,如此反复。M并不保留G状态,这是G可以跨M调度的基础。
下面是G、P、M定义的代码片段:

//src/runtime/runtime2.go
type g struct {
        stack      stack   // offset known to runtime/cgo
        sched     gobuf
        goid        int64
        gopc       uintptr // pc of go statement that created this goroutine
        startpc    uintptr // pc of goroutine function
        ... ...
}

type p struct {
    lock mutex

    id          int32
    status      uint32 // one of pidle/prunning/...

    mcache      *mcache
    racectx     uintptr

    // Queue of runnable goroutines. Accessed without lock.
    runqhead uint32
    runqtail uint32
    runq     [256]guintptr

    runnext guintptr

    // Available G's (status == Gdead)
    gfree    *g
    gfreecnt int32

  ... ...
}

type m struct {
    g0      *g     // goroutine with scheduling stack
    mstartfn      func()
    curg          *g       // current running goroutine
 .... ..
}

2、G被抢占调度

和操作系统按时间片调度线程不同,Go并没有时间片的概念。如果某个G没有进行system call调用、没有进行I/O操作、没有阻塞在一个channel操作上,那么m是如何让G停下来并调度下一个runnable G的呢?答案是:G是被抢占调度的。

前面说过,除非极端的无限循环或死循环,否则只要G调用函数,Go runtime就有抢占G的机会。Go程序启动时,runtime会去启动一个名为sysmon的m(一般称为监控线程),该m无需绑定p即可运行,该m在整个Go程序的运行过程中至关重要:

//$GOROOT/src/runtime/proc.go

// The main goroutine.
func main() {
     ... ...
    systemstack(func() {
        newm(sysmon, nil)
    })
    .... ...
}

// Always runs without a P, so write barriers are not allowed.
//
//go:nowritebarrierrec
func sysmon() {
    // If a heap span goes unused for 5 minutes after a garbage collection,
    // we hand it back to the operating system.
    scavengelimit := int64(5 * 60 * 1e9)
    ... ...

    if  .... {
        ... ...
        // retake P's blocked in syscalls
        // and preempt long running G's
        if retake(now) != 0 {
            idle = 0
        } else {
            idle++
        }
       ... ...
    }
}

sysmon每20us~10ms启动一次,按照《Go语言学习笔记》中的总结,sysmon主要完成如下工作:

  • 释放闲置超过5分钟的span物理内存;
  • 如果超过2分钟没有垃圾回收,强制执行;
  • 将长时间未处理的netpoll结果添加到任务队列;
  • 向长时间运行的G任务发出抢占调度;
  • 收回因syscall长时间阻塞的P;

我们看到sysmon将“向长时间运行的G任务发出抢占调度”,这个事情由retake实施:

// forcePreemptNS is the time slice given to a G before it is
// preempted.
const forcePreemptNS = 10 * 1000 * 1000 // 10ms

func retake(now int64) uint32 {
          ... ...
           // Preempt G if it's running for too long.
            t := int64(_p_.schedtick)
            if int64(pd.schedtick) != t {
                pd.schedtick = uint32(t)
                pd.schedwhen = now
                continue
            }
            if pd.schedwhen+forcePreemptNS > now {
                continue
            }
            preemptone(_p_)
         ... ...
}

可以看出,如果一个G任务运行10ms,sysmon就会认为其运行时间太久而发出抢占式调度的请求。一旦G的抢占标志位被设为true,那么待这个G下一次调用函数或方法时,runtime便可以将G抢占,并移出运行状态,放入P的local runq中,等待下一次被调度。

3、channel阻塞或network I/O情况下的调度

如果G被阻塞在某个channel操作或network I/O操作上时,G会被放置到某个wait队列中,而M会尝试运行下一个runnable的G;如果此时没有runnable的G供m运行,那么m将解绑P,并进入sleep状态。当I/O available或channel操作完成,在wait队列中的G会被唤醒,标记为runnable,放入到某P的队列中,绑定一个M继续执行。

4、system call阻塞情况下的调度

如果G被阻塞在某个system call操作上,那么不光G会阻塞,执行该G的M也会解绑P(实质是被sysmon抢走了),与G一起进入sleep状态。如果此时有idle的M,则P与其绑定继续执行其他G;如果没有idle M,但仍然有其他G要去执行,那么就会创建一个新M。

当阻塞在syscall上的G完成syscall调用后,G会去尝试获取一个可用的P,如果没有可用的P,那么G会被标记为runnable,之前的那个sleep的M将再次进入sleep。

四、调度器状态的查看方法

Go提供了调度器当前状态的查看方法:使用Go运行时环境变量GODEBUG。

$GODEBUG=schedtrace=1000 godoc -http=:6060
SCHED 0ms: gomaxprocs=4 idleprocs=3 threads=3 spinningthreads=0 idlethreads=0 runqueue=0 [0 0 0 0]
SCHED 1001ms: gomaxprocs=4 idleprocs=0 threads=9 spinningthreads=0 idlethreads=3 runqueue=2 [8 14 5 2]
SCHED 2006ms: gomaxprocs=4 idleprocs=0 threads=25 spinningthreads=0 idlethreads=19 runqueue=12 [0 0 4 0]
SCHED 3006ms: gomaxprocs=4 idleprocs=0 threads=26 spinningthreads=0 idlethreads=8 runqueue=2 [0 1 1 0]
SCHED 4010ms: gomaxprocs=4 idleprocs=0 threads=26 spinningthreads=0 idlethreads=20 runqueue=12 [6 3 1 0]
SCHED 5010ms: gomaxprocs=4 idleprocs=0 threads=26 spinningthreads=1 idlethreads=20 runqueue=17 [0 0 0 0]
SCHED 6016ms: gomaxprocs=4 idleprocs=0 threads=26 spinningthreads=0 idlethreads=20 runqueue=1 [3 4 0 10]
... ...

GODEBUG这个Go运行时环境变量很是强大,通过给其传入不同的key1=value1,key2=value2… 组合,Go的runtime会输出不同的调试信息,比如在这里我们给GODEBUG传入了”schedtrace=1000″,其含义就是每1000ms,打印输出一次goroutine scheduler的状态,每次一行。每一行各字段含义如下:

以上面例子中最后一行为例:

SCHED 6016ms: gomaxprocs=4 idleprocs=0 threads=26 spinningthreads=0 idlethreads=20 runqueue=1 [3 4 0 10]

SCHED:调试信息输出标志字符串,代表本行是goroutine scheduler的输出;
6016ms:即从程序启动到输出这行日志的时间;
gomaxprocs: P的数量;
idleprocs: 处于idle状态的P的数量;通过gomaxprocs和idleprocs的差值,我们就可知道执行go代码的P的数量;
threads: os threads的数量,包含scheduler使用的m数量,加上runtime自用的类似sysmon这样的thread的数量;
spinningthreads: 处于自旋状态的os thread数量;
idlethread: 处于idle状态的os thread的数量;
runqueue=1: go scheduler全局队列中G的数量;
[3 4 0 10]: 分别为4个P的local queue中的G的数量。

我们还可以输出每个goroutine、m和p的详细调度信息,但对于Go user来说,绝大多数时间这是不必要的:

$ GODEBUG=schedtrace=1000,scheddetail=1 godoc -http=:6060

SCHED 0ms: gomaxprocs=4 idleprocs=3 threads=3 spinningthreads=0 idlethreads=0 runqueue=0 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0
  P0: status=1 schedtick=0 syscalltick=0 m=0 runqsize=0 gfreecnt=0
  P1: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0
  P2: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0
  P3: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0
  M2: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 helpgc=0 spinning=false blocked=false lockedg=-1
  M1: p=-1 curg=17 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=false lockedg=17
  M0: p=0 curg=1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 helpgc=0 spinning=false blocked=false lockedg=1
  G1: status=8() m=0 lockedm=0
  G17: status=3() m=1 lockedm=1

SCHED 1002ms: gomaxprocs=4 idleprocs=0 threads=13 spinningthreads=0 idlethreads=7 runqueue=6 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0

 P0: status=2 schedtick=2293 syscalltick=18928 m=-1 runqsize=12 gfreecnt=2
  P1: status=1 schedtick=2356 syscalltick=19060 m=11 runqsize=11 gfreecnt=0
  P2: status=2 schedtick=2482 syscalltick=18316 m=-1 runqsize=37 gfreecnt=1
  P3: status=2 schedtick=2816 syscalltick=18907 m=-1 runqsize=2 gfreecnt=4
  M12: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=true lockedg=-1
  M11: p=1 curg=6160 mallocing=0 throwing=0 preemptoff= locks=2 dying=0 helpgc=0 spinning=false blocked=false lockedg=-1
  M10: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=true lockedg=-1
 ... ...

SCHED 2002ms: gomaxprocs=4 idleprocs=0 threads=23 spinningthreads=0 idlethreads=5 runqueue=4 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0
  P0: status=0 schedtick=2972 syscalltick=29458 m=-1 runqsize=0 gfreecnt=6
  P1: status=2 schedtick=2964 syscalltick=33464 m=-1 runqsize=0 gfreecnt=39
  P2: status=1 schedtick=3415 syscalltick=33283 m=18 runqsize=0 gfreecnt=12
  P3: status=2 schedtick=3736 syscalltick=33701 m=-1 runqsize=1 gfreecnt=6
  M22: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=true lockedg=-1
  M21: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=true lockedg=-1
... ...

关于go scheduler调试信息输出的详细信息,可以参考Dmitry Vyukov的大作:《Debugging performance issues in Go programs》。这也应该是每个gopher必读的经典文章。当然更详尽的代码可参考$GOROOT/src/runtime/proc.go中的schedtrace函数。


微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



Statcounter View My Stats