对golang多核编程的一点了解

sysu_huyh5 · · 4352 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

/*1、本博客的原创文章,都是本人平时学习所做的笔记,禁止转载,谢谢合作。
2、本博客的文章,未注明原创的,转载自其它博客或网站,作为自己的参考资料,感谢这些文章的原创人员。
3、如有侵犯您的知识产权和版权问题,请通知本人,本人会即时做出处理删除文章。
4、如果本博客的文章在知识点上有错误,欢迎指出错误所在,欢迎多多交流。谢谢!
5、本博客的目的是知识交流所用,他人在此留言并不代表本人同意、支持或者反对此观点。
6、本博客的文章、图片以及转载的文章引起的任何意外、疏忽、合约毁坏、诽谤、版权或知识产权侵犯及其所造成的损失(包括因下载而感染电脑病毒),本人概不负责,亦不承担任何法律责任。
7、因本博客的文章、图片以及转载的文章所产生的任何法律(包括宪法,民法,刑法,书法,公检法,基本法,劳动法,婚姻法,输入法,没办法,国际法,今日说法,吸星大法,与台湾关系法及文中涉及或可能涉及、以及未涉及之法,各地治安管理条例)纠纷或责任本人概不负责。
8、一切网民在进入本博客的各个页面时,默认表示遵守本条款并完全同意。
9、此声明最终解释权归本人所有。
*/

 

 如何解决多核编程的问题,无非就是如何更好地屏蔽硬件上的复杂特性,例如缓存、一致性、内存屏障、原子操作等等,给开发者简单高效的并发特性。从这点出发,golang可以在内存中创建成千上万的协程,并且提供了协程间通信的基础设施,且golang自带汇编器和链接器。

 

    讲到golang的协程goroutine,就不得不提golang groutine和操作系统os的线程之间区别,便于我们理解golang底层并发的相关概念。

    1 栈的区别。每一个os线程都有一个固定大小的内存块(一般会是2MB)来做做栈,这个栈会用来存储当前os thread调用或挂起的函数的内部变量。这个固定大小的栈同时很大,又很小。很小来源于现在服务器机器内存基本都是几十个G。很大又可以理解成,如果golang里面goroutine也是使用如此固定大小的栈的话,那么成千上万个goroutine就不大可能了。除去大小的问题之外,固定大小的栈对于更复杂或者更深层次的递归函数调用来说显然是不够的。相反,golang的goroutine会以一个很小的栈开始其生命周期,一般只需要2KB,这个栈大小会根据需要动态地伸缩,其最大值能达到1GB。

    2 调度方式不同。os thread会被操作系统内核调度,每隔几毫秒,一个硬件计时器便会冲断处理器,调用一个叫做scheduler的内涵函数。这个函数会挂起当前执行的线程并保存内存中它寄存器内存,检查线程列表并决定下一次哪个线程可以被运行,并从内存中恢复该线程的寄存器信息,然后恢复执行该线程的现场并开始执行线程,这就是我们所谓的上下文切换过程,通常这个步骤是比较慢的(对于高性能要求来说),因为其局部性很差,需要几次内存访问,并且会增加运行的cpu周期。而golang的运行就包含了自己的调度器,这个调度器使用了一些技术手段,比如m:n调用,因为其会在n个操作系统线程上多工调用m个goroutine。和os的thread不同,go调度器并不是使用硬件定时器,goroutine调度并不需要进入内核上下文,所以goroutine调度比一个os thread调度代价要低得多。(稍后我们详细讲解下相关调度方案,并对比erlang调度方案)

        Go 的新调度器模型主要涉及3个核心概念:M、P及G,如下图所示:

    M 代表OS的线程,P代表当前系统的处理器数(一般由GOMAXPROCS 环境变量指定),G代表Go语言的用户级线程,也就是通常所说的 Goroutine。

    在多核平台上,P的数量一般对应处理器核心或硬件线程的数量,调度器需要保证所有的P都有G执行,以保证并行度。

M 必须与P绑定方能执行任务G,如下图所

 在旧版 Go 调度器实现中,由于缺少P, 一旦运行 G (goroutine)的 M (OS线程)陷入阻塞状态(如调用某个阻塞的系统调用)时,M 对应的 OS 线程就会被操作系统调度出去,从而导致系统中其他就绪的G也不能执行;而添加了P这个逻辑结构后,一旦发生上述情况,阻塞的 M 将被与其对应的 P 剥离,RUNTIME会再分配一个 M 并将其与已经剥离出来的 P 绑定,运行其他就绪的G。这个过程如下图所示:

 在实际实现中,考虑到代码执行的局部性因素,一般会倾向于推迟 M 与 P 剥离的时机。具体来说,RUNTIME中存在一个驻留的线程sysmon,用来监控所有进入Syscall 的 M,只有当 Syscall 执行时间超出某个阈值时,才会将 M 与 P 分离。

        另外一个保证系统运行稳定性的方式是负载均衡机制,在Go中,用了 “任务窃取” 的方法。

        首先介绍一下 Go 的任务队列,每个 P 都有一个私有的任务队列 (实现上是一个用数组表示的循环链表)以及一个公共队列(单链表表示),私有队列的功能是为了减轻公共队列的竞争开销。

    当一个 P 的私有任务队列为空时,它会从全局队列中寻找就绪态的 G 执行;如果全局队列也为空,则会随机选择窃取其他 P 私有执行队列中的任务G,从而保证所有线程尽可能以最大负载工作。其示意图如下:

  由于 P 的私有队列采用了数组结构,很容易计算出队列中间的位置,因此“窃取者” 采用了与 “被窃取者” 均分任务的方法,以尽可能达到负载均衡。

    无论从公共队列取任务还是进行“窃取”,都会引起一定的竞争开销,因此 RUNTIME 会倾向于将新建任务或新转变为就绪态的任务添加到当前执行 P 的私有队列中。 仅当执行的任务调用 yield 机制让出处理器或进入了一个长时间执行的系统调用时,该任务才会被添加到公共队列中。

    与 Go 不同,Erlang 的调度器是一个轮转而非协作式的调度器,每个进程创建时会被分配一个称为“reduction”的值,是一个计算量的度量(基本上等同于函数调用的次数),类似 OS 的时间片。进程每执行一定量的计算后,reduction值就会累计,一旦达到阈值,该进程就会给切换出去。这种调度方式在 Erlang 中被称为 “reduction-counting”。

        采用轮转的调度方式能更好的防止程序设计不当而导致的个别进程饿死的情况,同时能够实现更好的实时性功能。

        同时,Erlang还为进程提供了四个不同的优先级:max,high,normal和low。不同优先级进程按优先级调度;同级进程按轮转方式调度。每个调度器包含3个任务队列,Max和High具有单独的队列,normal和low则位于同一个队列 —— 调度器忽略一定次数的low级进程来实现二者间的差别。

        Erlang 调度器之所以能够实现优先级轮转调度,主要是得益于其基于虚拟机的执行方式:由于每条Erlang指令都需要经过 BEAM 解释执行,因此 process 的运行完全处于BEAM的监控之下,BEAM可以方便的完成对进程的切换。与之相对,由于 Go 的 Goroutine 与 RUNTIME 都是 Native 执行的,其在执行上的地位是平等的,RUNTIME 没有能力切换一个执行中的 Goroutine,除非其自己调出或调用RUNTIME 功在 ,因而只能实现协作调度。

 通过上述简单对比,我们大体上了解了Erlang和Go两种语言在并发任务调度上的异同,可以说二者各有优缺点:Go 的调度模型更加高效(Native)而 Erlang 则提供了更强大的功能(实时性、优先级)。


有疑问加站长微信联系(非本文作者)

本文来自:开源中国博客

感谢作者:sysu_huyh5

查看原文:对golang多核编程的一点了解

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

4352 次点击  ∙  2 赞  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传