Go语言进阶:调度器系列(1)起源

去语言中文网,致力于每天分享代码,开源等知识,欢迎关注我,会有意想不到的收获!

如果将这种语言比作武侠小说中的武术,如果仅使用它,它将达到四到五层。如果使用它,它将是六层或七层。如果你可以看到移动,你将有8或9层。如果你着迷,如果你着迷,十个楼层立于不败之地。

如果你想真正掌握一门语言,你需要获得超过八层,你需要了解语言各个方面的细节。

我希望将来可以有八到九层Go语言。我怎么能不理解调度程序!

谷歌,百度,微信搜索了很多Go语言调度文章,这些文章提出来讨论调度程序是什么,它是什么构成,它是如何工作的,我只能从这些分散的文章中形成一个调度程序。 “个人资料”,这是我想要的结果,但还不够。

学习不仅要知道它是什么,还要知道原因。

在你学习之前,要学习知识的历史并学习知识,这样你才能理解为什么会这样。

因此,我打算写一系列goroutine调度文章,从历史背景开始,一步一步,希望大家都能全面了解goroutine调度程序。

本文介绍了与调度程序相关的历史背景,请慢慢阅读。

上面的大个子是ENIAC,出生于宾夕法尼亚大学。它是世界上第一台真正的通用计算机。与现代计算机相比,它具有相当“笨重”,其计算能力和现代智能手机的普及。相比之下,它只是一个地下,ENIAC在地下,智能手机在天空中。

它上面没有操作系统,更不用说进程,线程和协同程序了。

后来,现代计算机有一个操作系统。每个程序都是一个进程,但操作系统只能运行一个进程一段时间。在该过程运行之前,可以运行下一个过程。这个时期可以成为一个单一的过程时代。连续时代。

与ENIAC相比,单个过程有数万次的提升,但仍然太慢。例如,如果进程阻塞数据,则会浪费CPU。伟大的程序员认为,不能浪费。啊,我们怎样才能充分利用CPU?

后来,操作系统具有最早的并发能力:多进程并发。当一个进程被阻塞时,它会切换到另一个等待执行的进程,这样就可以尽可能地利用CPU,并且不会浪费CPU。

多进程真的是一件好事。在能够安排流程之后,优秀的程序员发现流程有太多资源。在创建,切换和销毁时,需要很长时间。 CPU使用它。但是,很大一部分CPU用于进程调度。我们如何提高CPU利用率?

每个人都希望拥有一个轻量级的进程,调度不需要花费太多时间,因此CPU有更多的时间来执行任务。

稍后,操作系统支持线程。线程正在进行中。线程比进程需要更少的资源来运行。与流程相比,切换只是“不是问题”。

一个进程可以有多个线程。当CPU执行调度时,它切换到一个线程。如果下一个线程也是当前进程,则仅切换线程,并且可以完成“快速”。如果下一个线程不是当前进程,则需要切换进程,这需要一点时间。

在这个时代,CPU调度在进程和线程之间切换。多线程看起来不错,但实际的多线程编程就像一瞥。一个是因为线程设计本身有点复杂,但是由于需要考虑许多低级细节,例如锁和碰撞检测。

多进程,多线程提高了系统的并发性,但在当今的高Internet并发场景中,为每个任务创建一个线程是不现实的,因为它占用了大量内存(每个线程的内存占用量为MB),调度更多线程后会占用大量CPU。伟大的程序员已经开始考虑如何充分利用CPU,内存和其他资源来实现更高的并发性?

由于线程的资源使用和调度是在高并发性的情况下,它仍然相对较大。有没有更轻量级的东西?

您可能知道线程分为内核模式线程和用户模式线程。用户模式线程需要绑定内核模式线程。 CPU不会感知用户模式线程的存在。它只知道它正在运行一个线程。这个线程实际上是内核。状态线程。

用户模式线程实际上有一个名为co-routine的名称。为了便于区分,我们使用协同程序来引用用户模式线程,并使用线程来引用内核状态线程。

用户级线程,应用程序级线程,绿色线程都指向同一个东西,这是一个OS无法察觉的线程。如果您有关于Google协同程序的信息,您会在绿色线程维基中看到它引用用户模式线程。百科全书,看看绿色线程的实现列表,你会看到很多协程实现,比如Java,Lua,Go,Erlang,Common Lisp,Haskell,Rust,PHP,Stackless Python,所以我觉得用户模式的线程是协程。

协程与线程不同。线程调度由CPU抢占。协程调度由用户状态协调。协程放弃CPU后,执行下一个协同程序。

协同程序和线程之间有三种映射关系:

N: 1,N群组绑定到1个线程。优点是协程完成用户状态线程中的切换并且不会进入内核状态。这种切换非常轻便快速。但也存在很大的弊端。进程的所有协同程序都绑定到一个线程。一个是程序不能使用硬件的多核加速功能,另一个是一旦协程被阻塞,导致线程被阻塞,进程其他协同程序无法执行,并且没有能力共存。 1: 1,1 coroutine绑定到1个线程,这是最容易实现的。协程的调度由CPU完成。 N: 1没有缺点,但是存在一个缺点,即协同程序的创建,删除和切换的成本由CPU完成,这有点昂贵。 M: N,M个co-routins绑定到N个线程,它是N: 1和1: 1类型的组合,克服了上述两个模型的缺点,但是实现起来最复杂。

Coroutine是一件好事,很多语言都支持协同程序,例如:Lua,Erlang,Java(C ++会支持),即使语言不支持,也有支持协同程序的库,比如C语言协程(风云) Daniel工作),Kotlin的kotlinx.coroutines,Python的gevent。

Go语言的诞生是为了支持高并发性,并且有两种支持高并发性的模型:CSP和Actor。由于Occam和Erlang都选择了CSP(来自Go FAQ)并且效果很好,Go也选择了CSP,但与前两者不同,Go使用频道作为一等公民。

由于上面描述的多线程编程太不友好,Go使用goroutine和channel来提供一种更简单的并发方式。 goroutine来自coroutine的概念,它允许一组可重用的函数在一组线程上运行。即使存在协程块,线程的其他协同程序也可以由运行时调度并传输到其他可运行的线程。最重要的是程序员无法看到底层细节,这降低了编程的难度并提供了更容易的并发性。

在Go中,coroutine被称为goroutine(Rob Pike说goroutine不是协程,因为它们不完全相同),它非常轻量级,goroutine只需要几KB,而这些KB足以让goroutine运行,这是可以在有限的内存空间中支持大量goroutine,支持更多的并发性。虽然goroutine堆栈只需要几千字节,但它实际上是可扩展的。如果需要更多内容,运行时将自动为goroutine分配它。

我终于来到了Go语言的调度员。

调度器的任务是在用户模式下完成goroutine调度,调度器的实现好坏,这对并发性有很大的影响,Go调度器是M: N类型,这是最多的实施起来很复杂。

当前的Go语言调度程序在2012年(设计)进行了重新设计。在此之前,调度程序称为旧调度程序。旧的调度程序实现得不是很好,并且存在性能问题。所以花了大约4年才能被替换,旧的调度程序可能是这样的:

底部是操作系统,中间是运行时,运行时在Go中非常重要,许多运行时运行的程序都是由运行时完成的,调度程序是运行时的一部分,虚拟线程是调度程序,它有两个重要组成部分:

M,代表线程,它必须运行goroutine。全局G队列,是全局goroutine队列,所有goroutine都存储在此队列中,goroutine由G表示。

M想要执行,放回G,必须访问全局G队列,并且M有多个,即需要锁定对同一资源的多线程访问以确保互斥/同步,因此全局G队列受到保护通过互斥锁。

旧调度程序有4个缺点:

创建,销毁和安排G都需要每个M获得一个锁,这会产生激烈的锁定竞争。 M传输G将导致延迟和额外的系统负载。例如,当G包含新协程的创建时,M创建G'。为了继续执行G,有必要将G'给M'执行,这也会导致较差的局部性,因为G'和G是相关的。最好把它放在M而不是其他M'上。 M中的Mcache用于存储小对象,mcache和堆栈与M相关联,导致大量内存开销和较差的局部性。系统调用会导致频繁的线程阻塞和解除阻塞操作,从而增加系统开销。

面对上述旧的调度问题,Go设计了一个新的调度程序,设计文件:

引入了新的调度程序:

P:处理器,包含运行goroutine的资源。如果线程想要运行goroutine,它必须首先获取P,并且P还包含可运行的G队列。工作窃取:当M-bound P没有可运行的G时,它可以从其他正在运行的M中窃取G.

现在,您在调度程序中拥有所有三个重要的缩写。所有文章都使用这些缩写,请记住:

G: goroutineM:工作线程P:处理器,其中包含运行Go代码的资源,M必须与P关联才能运行G.

本文的目的不是介绍调度程序的实现,而是调度程序的一些思想可以帮助您更好地理解调度程序的实现,因此我们返回调度程序的设计概念。

调度程序有两个主要的想法:

重用线程:协程本身在一组线程上运行,不需要经常创建和销毁线程,但重用线程。有两种方法可以在调度程序中重用线程:1)工作窃取,当此线程没有可运行的G时,尝试从其他线程绑定的P中窃取G,而不是破坏线程。 2)切换,当线程被G的系统调用阻塞时,线程释放绑定的P并将P传送到其他空闲线程以供执行。

使用parallel:GOMAXPROCS设置P的数量。当GOMAXPROCS大于1时,最多运行GOMAXPROCS线程。这些线程可以同时分布在多个CPU核心上,使并发使用并行。此外,GOMAXPROCS还限制了并发度,例如GOMAXPROCS=core/2,它使用高达一半的CPU内核来实现并行性。

调度程序的两个小策略:

抢占:在协程中,等待协程使CPU执行下一个协同程序。在Go中,goroutine占用高达10ms的CPU,防止其他goroutine挨饿。这是一个goroutine与coroutine不同的地方。

全局G队列:新调度程序中仍有一个全局G队列,但该功能已被削弱。当M执行工作窃取并且不能从其他P窃取G时,它可以从全局G队列获得G.

上面提到的并行性,关于并发性和并行性:Go创始人Rob Pike一直强调go是并发的,而不是并行的,因为Go在一段时间内做了数十万甚至数百万的工作,而且没有做很多工作同时。并发可以利用并行性来提高效率,并且调度程序是并行设计的。

并行取决于多核技术。每个核心上一次只能执行一个线程。当我们的CPU有8个内核时,我们可以同时执行8个线程。这是平行的。

本文的主要目的是为Go语言调度程序奠定基础。我将以远近的方式介绍多进程,多线程,协程,并发和并行的“历史”。我希望你理解为什么Go采用Goroutine,为什么调度程序如此重要。

如果您不赶时间,并且想要了解Go调度程序的原理,请阅读以下文章:

设计:代码中调度程序的描述:最多引用调度文章: PPT,目前看到最好的PPT调度:偷纸:分析调度程序的文件(只问你6 6,有论文):

免责声明:尚未完全搜索有关旧调度程序的信息。根据新调度程序设计的描述,想象一下编写旧调度程序的章节,这可能是错误的。

参考

(computing)#(computing)# #如果这篇文章对您有所帮助,您可能希望关注我的Github并收到一篇文章。作者:big bin,如果你喜欢原版授权发布的这篇文章,请随时转载,但请保留此说明链接: