Go语言 Go1.1新调度器详解

原创翻译文章,转载请注明出处:服务器非业余研究-sunface

简介

Go1.1更新中最重要的特性之一就是由Dmitry Vyukov开发的全新的调度器。新的调度器能极大的提高Go并行程序的性能并且不需要对程序进行修改,因此我认为应该写一篇文章为大家介绍下新版的调度器特性。

这篇文章所写的绝大部分内容都可以在original design doc找到 ——这是篇很有技术含量而且很通俗易懂的文章,你所需要的知识都可以从这篇文章中获取,而且由于插入了配图,因此相对来说更清晰易读。

Go运行时系统需要用调度器来做什么?

首先我们需要了解下为什么需要新的调度器?为什么在操作系统能对线程进行调度的情况下还要创建一个用户空间的调度器?

对现存的Unix进程模型来说,POSIX线程API是一种逻辑上扩展,线程在许多控制方式上和进程是一样的。线程拥有自己的信号掩码,并且可以被绑定到CPU,也可以被放入linux Cgroups并查询这些线程使用了哪些资源。这些线程的特性对于使用goroutine的Go程序来说并不怎么需要,而且当你拥有超过100000线程的时候,你会发现非常难以控制如此庞大的系统。

另一个问题是操作系统在基于Go模型的时候往往并不能做出正确的调度。比如,Go的垃圾回收器(GC)在回收的时候需要所有的线程都停止并且要求所有线程的内存都保持一致的状态,所以GC会等待一个正在运行的线程,直到该线程执行到某个内存会达到一致状态的点。

当你在某个时间点有许多线程需要被调度的时候,有可能会发生这种情况:你要等待这些线程都达到内存一致的状态,因为只有在Go调度器知道了线程的内存达到一致状态后才会继续执行调度。这样会导致当程序停下来进行GC的时候,我们只有等待,等待那些在某个CPU上还继续活跃的线程。

接下来谁会是我们的主角呢?

有三种线程模型是较为常用的 :第一种是N:1,多个用户空间线程会运行在同一个OS线程上,这种模型上下文切换很迅速但是并不能利用多核系统的优势;第二种是1:1模型,一个线程对应一个OS线程,这种模型下可以利用机器的所有CPU核心,但是上下文切换比较慢,因为要经过系统层的切换。Go通过使用M:N的映射方式来利用前两种模型的优点。它会在任意数量的OS线程上调度任意数量的goroutines,这样既能获得快速的上下文切换也能利用系统中的所有CPU核心,但是也有不利的地方:会增加调度器调度的复杂度。

Go使用了三种实体模型来实现调度:

三角形代表了OS线程,它是由OS来管理执行的线程并且工作模式很像标准的POSIX线程,在运行时代码中,用M来代表机器(machine)。

圆形代表一个goroutine,包含了栈、指令指针和调度该goroutine所需的其他重要信息,比如可能阻塞该goroutine的任何channel,在运行时代码中,用G来代表goroutine.

矩形代表调度过程的上下文(context),可以看作是一个在单独线程上运行Go代码的调度器的本地化版本(localized version),它是让我们从N:1调度器映射变为M:N调度器映射的重要组成部分,在运行时代码中,用P代表处理器。

这里有两个线程(M),每一个都拥有一个上下文(P),每一个都运行着一个gorotine(G )。为了能运行gorotines,一个线程必须拥有一个上下文.

系统上下文的数量是在启动的时候被设置为环境变量GOMAXPROCS或者runtime.GOMAXPROCS(),事实上上下文的数量是固定的,所以在任何时候都只有GOMAXPROCS个Go代码段在运行,比如4核心的PC会在4个线程上运行着4段Go代码。

灰色的goroutine虽然没有在运行,但是做好了被调度的准备。它们被排列在runqueues列表中。无论在何时,某个goroutine执行了go语句后都会在runqueue的末端添加该goroutine。一旦一个goroutine能被调度时,运行这个goroutine的上下文就会把该goroutine弹出runqueue,设置栈和指令指针后就开始运行这个goroutine。

为了减少互斥和竞争(mutex contention),每个上下文都有自己本地的runqueue。而早期版本的调度器仅仅维护着一个使用互斥量(mutex)保护的全局runqueue,这种情况下线程会经常因为等待互斥量解锁而被阻塞,如果你想在32核心的机器上使用多个goroutines来压榨机器性能,可能会发现因为早期版本调度器的原因,性能反而变得很糟糕。

只要上下文有goroutine需要运行,调度器就会持续稳定的进行调度,然而有些场景可能会改变这种稳定调度的状况。

你准备调用我们的哪位主角呢?

你可能会想为什么我们需要上下文这小编?为什么我们不能抛开上下文然后把runqueues放在线程上呢?其实我们使用上下文的原因是:即使正在运行的线程阻塞了,也可以切换到其他线程继续处理。其中一个例子就是当我们执行系统调用的时候,线程就得进入阻塞,因为线程无法在被系统调用阻塞的同时还继续运行代码,所以需要通过上下文切换来继续调度运行代码。

这里我们能看到其中一个线程放弃了它的上下文,这样其他线程就可以运行在这个上下文中,调度器会保证有足够的线程能运行所有的上下文。为了能处理这次系统调用,可能会创建或者从线程缓存(thread cache)中获取上图中的M1。执行系统调用的那个线程会继续持有那个执行了系统调用的goroutine,因为这个线程虽然被OS阻塞了,但是技术上来说还是在运行的,因此不会goroutine不会被切换出去。

当系统调用返回的时候,为了继续运行返回的goroutine,当前线程必须尝试获取上下文。通常的模式是从其它线程中的一个窃取上下文,如果不能窃取的话,当前线程会将goroutine放入全局(global)runqueue中,然后将自己放入线程缓存中并进入休眠。

当上下文执行完本地runqueue后会从全局runqueue获取goroutines,上下文也会周期性的检查全局runqueue,否则全局runqueue中的goroutines永远都不会被运行。

上面这种处理系统调用的方法就是Go程序甚至能在GOMAXPROCS被设置为1的时运行在多个线程上的原因,因此运行时(runtime)使用goroutines来调用系统调用而不是使用线程。

如何窃取?

另外一种能改变调度器持续稳定调度的情况是:某个上下文的runqueque中再没有要被调度的goroutine。这种改变当各个runqueque之间分配的工作不平衡的时候也会发生,这个会导致在某个上下文runqueue空了后,该上下文就会自己结束,甚至会发生在系统中还有工作等待运行的时候。因此为了保持Go代码的运行,上下文可以从全局runqueque中获取goroutines,但是如果全局runqueque中没有goroutines的话,该上下文不得不从其他上下文runqueue中窃取goroutines,大约窃取另一个上下文runqueue中一般的内容。这种方法可以保证每个上下文都有工作可以做,也可以保证所有的线程都尽最大的能力努力工作。

何去何从

调度器还有很多细节:cgo线程LockOSThread()函数,集成了网络轮询(network poller)。这些已经超出了此文章的范围,但是仍然很值得学习。在Go的运行时库中还有很多很多有趣的事物等待着我们去探索开发,以后我也会继续写一些相关的文章进行介绍。

原文作者 Daniel Morsing

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


类型转换 1、int转string 2、string转int 3、string转float 4、用户结构类型转换
package main import s "strings" import "fmt" var p = fmt.Println func main() { p("Contains: ", s.Contains("test&quo
类使用:实现一个people中有一个sayhi的方法调用功能,代码如下: 接口使用:实现上面功能,代码如下:
html代码: beego代码:
1、读取文件信息: 2、读取文件夹下的所有文件: 3、写入文件信息 4、删除文件,成功返回true,失败返回false
配置环境:Windows7+推荐IDE:LiteIDEGO下载地址:http://www.golangtc.com/downloadBeego开发文档地址:http://beego.me/docs/intro/ 安装步骤: 一、GO环境安装 二、配置系统变量 三、Beego安装 一、GO环境安装 根
golang获取程序运行路径:
Golang的文档和社区资源:为什么它可以帮助开发人员快速上手?
Golang:AI 开发者的实用工具
Golang的标准库:为什么它可以大幅度提高开发效率?
Golang的部署和运维:如何将应用程序部署到生产环境中?
高性能AI开发:Golang的优势所在
本篇文章和大家了解一下go语言开发优雅得关闭协程的方法。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。1.简介本文将介绍首先为什么需要主...
这篇文章主要介绍了Go关闭goroutine协程的方法,具有一定借鉴价值,需要的朋友可以参考下。下面就和我一起来看看吧。1.简介本文将介绍首先为什么需要主动关闭gor...
本篇文章和大家了解一下go关闭GracefulShutdown服务的几种方法。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。目录Shutdown方法Regi...
这篇文章主要介绍了Go语言如何实现LRU算法的核心思想和实现过程,具有一定借鉴价值,需要的朋友可以参考下。下面就和我一起来看看吧。GO实现Redis的LRU例子常
今天小编给大家分享的是Go简单实现多租户数据库隔离的方法,相信很多人都不太了解,为了让大家更加了解,所以给大家总结了以下内容,一起往下看吧。一定会...
这篇“Linux系统中怎么安装NSQ的Go语言客户端”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希
本文小编为大家详细介绍“怎么在Go语言中实现锁机制”,内容详细,步骤清晰,细节处理妥当,希望这篇“怎么在Go语言中实现锁机制”文章能帮助大家解决疑惑,下面...
今天小编给大家分享一下Go语言中interface类型怎么使用的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考