从Golang调度器的作者视角探究其设计之道!
Posted 云加社区
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从Golang调度器的作者视角探究其设计之道!相关的知识,希望对你有一定的参考价值。
导语 | Golang核心开发人员、goroutine调度的设计者Dmitry Vyukov,在2019年的一个talk里深入浅出地阐述了goroutine调度的设计思想以及一些优化的细节。本文是笔者结合自身经验和认知的一点观后感,采用从零开始层层递进的方法,总结剖析了其背后的软件设计思想,希望对读者更好地理解goroutine调度GMP模型会有所帮助。
前言
视频地址:
https://2019.hydraconf.com/2019/talks/7336ginp0kke7n4yxxjvld/
这个视频我以前看过,近几天刷到便又看了一遍,真是有听君一席话受益匪浅之感。毫不夸张地说,本视频在笔者看过的所有资料中,对于GMP为什么要有Processor这点,讲得最为清楚。视频中对goroutine调度模型的讲解,真可谓深入浅出!下面笔者将自己的一些观感整理分享给大家,还没看过视频的同学,建议先看完本文再去看,收获会更大。
为了表达方便,本文会沿用golang里面的GMP缩写:
G —— goroutine
M —— 机器线程
P —— 对处理器的抽象
一、设计并发编程模型
goroutine调度的设计目标,其实就是设计一种高效的并发编程模型:
从开发的角度只需要一个关键词(go)就能创建一个执行会话,很方便使用,即开发效率是高效的。
从运行态的角度,上述创建的会话也能高效的被调度执行,即运行效率也是高效的。
我们可以近似将goroutine看待为协程(一些代码逻辑+一个栈上下文),如果读者用C/C++造过协程框架的轮子,会很容易理解这点。
注:除了高效之外,还有其他几个目标,如无大小限制的goroutine栈,公平的调度策略等。
二、从零开始:从多线程说起
想要实现并发的执行流,最直截了当的,自然就是多线程。由此便得出初始思路:每个goroutine对应一个线程。
从并发的功能角度来讲,该方案固然可以实现并发,但性能方面却很不堪,尤其是在并发很重的时候,成千上万个线程的资源占用、创建销毁、调度带来的开销会很巨大。
三、更进一步:线程池的方案
既然线程太多不好,那我们可以很轻易地做出一点改善,控制一下线程数量,如此便得到更进一步的方案:线程池,限定只启动N个线程。
由于该方案下,可能是M个goroutine,N个线程,因而显然需要考虑一个问题:对于一个goroutine,它到底该由哪个线程去执行?我们可以简单地采用一个全局的Global Run Queue,然后让所有线程主动去获取goroutine来执行,示意如下:
这样做在线程少的时候,如果调度行为不是很频繁,可能问题不大。但当线程较多时,就会有scalable的问题,mutex的互斥竞争会非常激烈(考虑到基于时间片的抢占行为,实际上调度必然是很频繁的)。
四、初具雏形:线程分治
在多线程编程领域中,互斥处理可以称得上是“名声在外”,需极其小心地去应对。最常见的解决方案,并不是如何精妙地去lock free,而是直接通过 “数据分治”和“逻辑分治”来避免做复杂的加锁互斥,将各个线程按横向(载荷分组)或纵向(逻辑划分)进行切分来处理工作。
通过数据分治的思想,我们就可以得到改进的方案:每个线程分别处理一批G,进行线程分治。将所有G分开放到各线程自己的存储中,即所谓的Local Run Queue中。示意如下:
注:Global Run Queue也还继续存在的,有关它存在的细节非本文重点,这里不做展开。
至此,调度模型已具雏形。
让我们继续分析确认一下,该模型是否真的解决了scalable的问题。上述模型下,为了充分利用CPU,每个线程要按一定的策略去Steal其他线程Local Run Queue里面的G来执行,以免线程之间存在load balance问题(有些太闲,有些又太忙)
因此在线程很多的时候,存在大量的无意义加锁Steal操作,因为其他线程的Local Run Queue可能也常常都是空的。还有另一个问题,由于现在的一些内存资源是绑定在线程上面的,会导致线程数量和资源占用规模紧耦合。当线程数量多的时候,资源消耗也会比较大。
注:在N核的机器环境下,假如我们设定线程池大小为N,由于系统调用的存在(关于系统调用的处理见后文),实际的线程数量会超过N。
五、趋于完善:将资源和线程解耦
既然每个线程一份资源也不合适,那么我们可以仿照线程池的思路,单独做一个资源池,做计算存储分离:把Local Run Queue及相关存储资源都挪出去,并依然限定全局一共N份,即可实现资源规模与系统中的真实线程数量的解耦。线程每次从对应的数据结构(Processor)中获取goroutine去执行,Local Run Queue及其他一些相关存储资源都挂在Processor下。这样加一层Processor的抽象之后,便得到众所周知的GMP模型:
现在的调度模型已趋于完善,不过前面我们主要侧重讲的是如何高效,还未讨论到调度的另一个关键问题:公平性与抢占,接下来我们看看如何实现抢占。
六、还要公平:调度抢占
参考操作系统CPU的调度策略,通常各进程会分时间片,时间片用完了就轮到其他进程。在golang里也可以如此,不能让一些goroutine长期霸占着运行资源不退出,必须实现基于时间片的“抢占”。
那怎么抢占呢,需要监测goroutine执行时间片是否用完了。如果要检查系统中的各种状态变化、事件发生情况,通常会有中断与轮询两种思路,中断是由一个中控方来做检查与控制,而轮询则是各个参与方按一定的策略主动check询问。因此对于goroutine抢占而言,有以下两种解决方案:
Signals,通过信号来中断原来的线程执行。
Cooperative checks,通过线程间歇性轮询自己check运行的时间片情况来主动暂停。
二者的优劣对比如下:
因为golang其实是有runtime的,而且代码编译生成也都是golang编译器控制的,综合优劣分析,选择后者会比较合理。
对于Cooperative checks的方案,从代码编译生成的角度看,很容易做check指令的埋点。且因为golang本来就要做动态增长栈,在函数入口处会插入检查是否该扩栈的指令,正好利用这一点来做相关的检查实现(这里有一些优化细节,可以使得基于时间片的抢占开销也较小)
插入check指令的做法,会导致该方案存在一个理论缺陷:若有一个死循环,里面的所有代码都不包含check指令,那依然会无法抢占,不过现实中基本不存在这种情况,总会做函数调用、访问channel等类似操作,因此不足为虑。
除此以外还有一个系统调用的问题,当线程一旦进入系统调用后,也会脱离runtime的控制。试想万一系统调用阻塞了呢,基于Cooperative checks的方案,此时又无法进行抢占,是不是整个线程也就罢工了。所以为了维持整个调度体系的高效运转,必然要在进入系统调用之前要做点什么以防患未然。Dmitry这里采用的办法也很直接,对于即将进入系统调用的线程,不做抢占,而是由它主动让出执行权。线程A在系统调用之前handoff让出Processor的执行权,唤醒一个idle线程B来做交接。当线程A从系统调用返回时,不会继续执行,而是将G放到run queue,然后进入idle状态等待唤醒,这样一来便能确保活跃线程数依然与Processor数量相同。
七、设计思想的小结
这里recap一下,把前文涉及到的一些软件设计思想罗列如下:
线程池,通过多线程提供更大的并发处理能力,同时又避免线程过多带来的过大开销。
资源池,对有一定规模约束的资源进行池化管理,如内存池、机器池、协程池等,前面的线程池也可以算作此类。
计算存储分离,分别从逻辑、数据结构两个角度进行设计,规划二者的耦合关系。
加一层,这个是万能大法,不赘述。
中断与轮询,用于监测系统中的各种状态变化、事件变化,通常来讲中断会比轮询更高效。
八、视频的其他内容
本文的重点在GMP模型,因此视频里还有一些其他的内容,文中并未详细展开:
Local Run Queue里面的G所创建的G会放到同样的Local Run Queue(如果满了还是会放GRQ),而且会限制被偷走,这样可以加强Locality,同时为了保证公平也做了时间片继承,以免不停创建G会长期霸占运行资源。
被抢占的G会放到全局的G队列(Global Run Queue),GRQ会每61次tick检查一次,Dmitry针对这个61解释了一番,但笔者认为还是有点拍脑袋的感觉。
G的栈采用的是Growable stack方案,在函数入口会有栈检查的指令,如需扩容栈,会拷贝到新申请的更大的栈。
Go runtime还会用Background thread来运行一些相对特别的G(如 Network Poller、Timer)。
以上这些内容,大家可以去视频学习。
注:本文基于2019的talk,不知最新版本的调度机制是否有进一步的调整,不过无论调整与否,这并不妨碍我们对GMP设计思想的学习。
九、进一步的改进
有同学在与笔者讨论时提了一个问题:还可以怎么继续优化,这真的是一个非常好的问题,这里将该问题的回答也放入文章。
不单纯针对GMP,话题稍微放大一点,下面简单聊聊goroutine调度机制的一些优化可能。
Dmitry自己在视频最后说的future work方向:
在很多cpu core的情况下,活跃线程数比较多,work steal的开销依旧有些浪费。
死循环不含cooperative check指令的这种edge情况的还没解决。
对于网络和timer的goroutine处理是使用全局方式的,不好scale。
以下纯属个人探讨:
首先整体上现在的模型已经比较完善,如何进一步优化要看实践场景遇到的问题,以及profile数据情况,只有问题和数据明确了,才清楚进一步工作的宏观重点(工作中也是,做性能优化需要有宏观视角)。
因为goroutine调度是属于协程类的调度,这里或许可以借鉴原来各种协程框架的思路做一些对比考虑。
由于笔者并没细看过代码,不大清楚work steal的overhead构成,或许可以设计其他的rebalance方式,例如换个视角,不是去steal,而是由runtime统一rebalance再收集派发。
目前就先想到这些,欢迎讨论。
十、欢乐游戏的协程框架
基于上面那个问题的回答,这里也补充介绍一下欢乐游戏协程框架(基于C++)中采用的处理机制,因为是纯业务自用,所以从设计要求上就低很多,不少点直接都可以不去考虑(这也说明了,有些时候再好的既有流行方案,从性能上讲可能也比不过自家的破轮子,当然自家的轮子泛化不足,肯定普适性就会差很多)
协程调度采用最简单的单线程模型
设计之初就没考虑用多线程充分利用多核资源,我们认为直接多部署一些进程就好。
对于一定要把单进程承载做的很高的极少数场景,可以专事专办,做专门的方案即可。
协程采用固定的栈大小
通常几百k就够了(例如256k或者512k),创建协程的时候就预分配好。
这点确实不如growable stack那么高明,但是从实践看也算够了,这样就免去了stack动态增长的工作(从应用编程的视角看,其实C++里我们可能因为无法做指令插入埋点,本来就做不到stack动态增长)。
我们在相邻stack之间加一些写保护page,这样一旦踩了就会 coredump。
同时通过编译选项,限制单层栈大小不能超过某个阈值。
协程调度完全不考虑公平性,全部采用主动handoff策略
对于某个协程,如果它要持续运行,就任它运行,直到要进行阻塞类操作(典型如RPC调用),才会交出执行权。实际上对于业务来讲,微观层面几十毫秒内哪个协程多占了一点执行权真的无所谓,不用太讲究公平性。假如真的有些协程饿死了,那说明业务都已经过载了(就是时时刻刻都在跑其他协程,cpu100),此时讨论公平也没什么意义了。假如我们真的要做,因为做不到指令插入,只能采用Signals信号中断的方式,在注册的信号处理函数中直接按需切栈。
主协程主控循环tick直接管理协程,协程调度不涉及background thread
网络IO、第三方异步API tick驱动、timer管理、协程创建销毁管理等都是主协程在做。
主控循环中,如果要创建或恢复协程,就任由它去立即执行,一直跑到它阻塞挂起再返回主协程。
协程切换示意图,图注:1、2、5在主协程,3、4在业务协程,主协程和业务协程都在主线程内。
仍可以有基于逻辑分治的多线程
框架不是真的只有一个线程,按功能拆分的日志线程,依然可以存在。
对于一些第三方异步API,如果其tick本身实现不好,导致大量占据了运行时间,也可以分拆线程,然后用队列之类的机制和主线程的主协程交互即可。
对于网络IO也同上。
总之,这种基于逻辑分治做线程拆分的改造都是很简单的,也并不会影响到核心协程调度的机制。
Golang - 调度剖析第一部分
简介
首先,Golang 调度器的设计和实现让我们的 Go 程序在多线程执行时效率更高,性能更好。这要归功于 Go 调度器与操作系统(OS)调度器的协同合作。不过在本篇文章中,多线程 Go 程序在设计和实现上是否与调度器的工作原理完全契合不是重点。重要的是对系统调度器和 Go 调度器,它们是如何正确地设计多线程程序,有一个全面且深入的理解。
本章多数内容将侧重于讨论调度器的高级机制和语义。我将展示一些细节,让你可以通过图像来理解它们是如何工作的,可以让你在写代码时做出更好的决策。因为原理和语义是必备的基础知识中的关键。
系统调度
操作系统调度器是一个复杂的程序。它们要考虑到运行时的硬件设计和设置,其中包括但不限于多处理器核心、CPU 缓存和 NUMA,只有考虑全面,调度器才能做到尽可能地高效。值得高兴的是,你不需要深入研究这些问题,就可以大致上了解操作系统调度器是如何工作的。
你的代码会被翻译成一系列机器指令,然后依次执行。为了实现这一点,操作系统使用线程(Thread)的概念。线程负责顺序执行分配给它的指令。一直执行没有指令为止。这就是我将线程称为“执行通路”的原因。
你运行的每个程序都会创建一个进程,每个进程都有一个初始线程。而后线程可以创建更多的线程。每个线程互相独立地运行着,调度是在线程级别而不是在进程级别做出的。线程可以并发运行(每个线程在单个内核上轮流运行),也可以并行运行(每个线程在不同的内核上同时运行)。线程还维护自己的状态,以便安全、本地和独立地执行它们的指令。
如果有线程可以执行,操作系统调度器就会调度它到空闲的 CPU 核心上去执行,保证 CPU 不闲着。它还必须模拟一个假象,即所有可以执行的线程都在同时地执行着。在这个过程中,调度器还会根据优先级不同选择线程执行的先后顺序,高优先级的先执行,低优先级的后执行。当然,低优先级的线程也不会被饿着。调度器还需要通过快速而明智的决策尽可能减少调度延迟。
为了实现这一目标,算法在其中做了很多工作,且幸运的是,这个领域已经积累了几十年经验。为了我们能更好地理解这一切,接下来我们来看几个重要的概念。
执行指令
程序计数器(PC),有时称为指令指针(IP),线程利用它来跟踪下一个要执行的指令。在大多数处理器中,PC指向的是下一条指令,而不是当前指令。
如果你之前看过 Go 程序的堆栈跟踪,那么你可能已经注意到了每行末尾的这些十六进制数字。如下:
1goroutine 1 [running]:
2 main.example(0xc000042748, 0x2, 0x4, 0x106abae, 0x5, 0xa)
3 stack_trace/example1/example1.go:13 +0x39 <- LOOK HERE
4 main.main()
5 stack_trace/example1/example1.go:8 +0x72 <- LOOK HERE
这些数字表示 PC 值与相应函数顶部的偏移量。+0x39
PC 偏移量表示在程序没中断的情况下,线程即将执行的下一条指令。如果控制权回到主函数中,则主函数中的下一条指令是0+x72
PC 偏移量。更重要的是,指针前面的指令是当前正在执行的指令。
1https://github.com/ardanlabs/gotraining/blob/master/topics/go/profiling/stack_trace/example1/example1.go
2
307 func main() {
408 example(make([]string, 2, 4), "hello", 10)
509 }
6
712 func example(slice []string, str string, i int) {
813 panic("Want stack trace")
914 }
十六进制数+0x39
表示示例函数内的一条指令的 PC 偏移量,该指令位于函数的起始指令后面第57条(10进制)。接下来,我们用 objdump 来看一下汇编指令。找到第57条指令,注意,runtime.gopanic
那一行。
1$ go tool objdump -S -s "main.example" ./example1
2TEXT main.example(SB) stack_trace/example1/example1.go
3func example(slice []string, str string, i int) {
4 0x104dfa0 65488b0c2530000000 MOVQ GS:0x30, CX
5 0x104dfa9 483b6110 CMPQ 0x10(CX), SP
6 0x104dfad 762c JBE 0x104dfdb
7 0x104dfaf 4883ec18 SUBQ $0x18, SP
8 0x104dfb3 48896c2410 MOVQ BP, 0x10(SP)
9 0x104dfb8 488d6c2410 LEAQ 0x10(SP), BP
10 panic("Want stack trace")
11 0x104dfbd 488d059ca20000 LEAQ runtime.types+41504(SB), AX
12 0x104dfc4 48890424 MOVQ AX, 0(SP)
13 0x104dfc8 488d05a1870200 LEAQ main.statictmp_0(SB), AX
14 0x104dfcf 4889442408 MOVQ AX, 0x8(SP)
15 0x104dfd4 e8c735fdff CALL runtime.gopanic(SB)
16 0x104dfd9 0f0b UD2 <--- 这里是 PC(+0x39)
记住: PC 是下一个指令,而不是当前指令。上面是基于 amd64 的汇编指令的一个很好的例子,该 Go 程序的线程负责顺序执行。
线程状态
另一个重要的概念是线程状态,它描述了调度器在线程中的角色。
线程可以处于三种状态之一: 等待中(Waiting)
、待执行(Runnable)
或执行中(Executing)
。
等待中(Waiting)
:这意味着线程停止并等待某件事情以继续。这可能是因为等待硬件(磁盘、网络)、操作系统(系统调用)或同步调用(原子、互斥)等原因。这些类型的延迟是性能下降的根本原因。
待执行(Runnable)
:这意味着线程需要内核上的时间,以便执行它指定的机器指令。如果有很多线程都需要时间,那么线程需要等待更长的时间才能获得执行。此外,由于更多的线程在竞争,每个线程获得的单个执行时间都会缩短。这种类型的调度延迟也可能导致性能下降。
执行中(Executing)
:这意味着线程已经被放置在一个核心上,并且正在执行它的机器指令。与应用程序相关的工作正在完成。这是每个人都想要的。
工作类型
线程可以做两种类型的工作。第一个称为 CPU-Bound,第二个称为 IO-Bound。
CPU-Bound:这种工作类型永远也不会让线程处在等待状态,因为这是一项不断进行计算的工作。比如计算 π 的第 n 位,就是一个 CPU-Bound 线程。
IO-Bound:这是导致线程进入等待状态的工作类型。比如通过网络请求对资源的访问或对操作系统进行系统调用。
上下文切换
诸如 Linux、Mac、 Windows 是一个具有抢占式调度器的操作系统。这意味着一些重要的事情。首先,这意味着调度程序在什么时候选择运行哪些线程是不可预测的。线程优先级和事件混在一起(比如在网络上接收数据)使得无法确定调度程序将选择做什么以及什么时候做。
其次,这意味着你永远不能基于一些你层经历过但不能保证每次都发生的行为来编写代码。如果应用程序中需要确定性,则必须控制线程的同步和协调管理。
在核心上交换线程的物理行为称为上下文切换。当调度器将一个正在执行的线程从内核中取出并将其更改状态为一个可运行的线程时,就会发生上下文切换。
上下文切换的代价是高昂的,因为在核心上交换线程会花费很多时间。上下文切换的延迟取决于不同的因素,大概在在 50 到 100 纳秒之间。考虑到硬件应该能够合理地(平均)在每个核心上每纳秒执行 12 条指令,那么一次上下文切换可能会花费 600 到 1200 条指令的延迟时间。实际上,上下文切换占用了大量程序执行指令的时间。
如果你在执行一个 IO-Bound 程序,那么上下文切换将是一个优势。一旦一个线程更改到等待状态,另一个处于可运行状态的线程就会取而代之。这使得 CPU 总是在工作。这是调度器最重要的之一,最好不要让 CPU 闲下来。
而如果你在执行一个 CPU-Bound 程序,那么上下文切换将成为性能瓶颈的噩梦。由于线程总是有工作要做,所以上下文切换阻碍了工作的进展。这种情况与 IO-Bound 类型的工作形成了鲜明对比。
少即是多
在早期处理器只有一个核心的时代,调度相对简单。因为只有一个核心,所以物理上在任何时候都只有一个线程可以执行。其思想是定义一个调度程序周期,并尝试在这段时间内执行所有可运行线程。算法很简单:用调度周期除以需要执行的线程数。
例如,如果你将调度器周期定义为 10ms(毫秒),并且你有 2 个线程,那么每个线程将分别获得 5ms。如果你有 5 个线程,每个线程得到 2ms。但是,如果有 1000 个线程,会发生什么情况呢?给每个线程一个时间片 10μs (微秒)?错了,这么干是愚蠢的,因为你会花费大量的时间在上下文切换上,而真正的工作却做不成。
你需要限制时间片的长度。在最后一个场景中,如果最小时间片是 2ms,并且有 1000 个线程,那么调度器周期需要增加到 2s(秒)。如果有 10000 个线程,那么调度器周期就是 20s。在这个简单的例子中,如果每个线程使用它的全时间片,那么所有线程运行一次需要花费 20s。
要知道,这是一个非常简单的场景。在真正进行调度决策时,调度程序需要考虑和处理比这更多的事情。你可以控制应用程序中使用的线程数量。当有更多的线程要考虑,并且发生 IO-Bound 工作时,就会出现一些混乱和不确定的行为。任务需要更长的时间来调度和执行。
这就是为什么游戏规则是“少即是多”。处于可运行状态的线程越少,意味着调度开销越少,每个线程执行的时间越长。完成的工作会越多。如此,效率就越高。
寻找一个平衡
你需要在 CPU 核心数和为应用程序获得最佳吞吐量所需的线程数之间找到平衡。当涉及到管理这种平衡时,线程池是一个很好的解决方案。将在第二部分中为你解析,Go 并不是这样做的。
CPU 缓存
从主存访问数据有很高的延迟成本(大约 100 到 300 个时钟周期),因此处理器核心使用本地高速缓存来将数据保存在需要的硬件线程附近。从缓存访问数据的成本要低得多(大约 3 到 40 个时钟周期),这取决于所访问的缓存。如今,提高性能的一个方面是关于如何有效地将数据放入处理器以减少这些数据访问延迟。编写多线程应用程序也需要考虑 CPU 缓存的机制。
数据通过cache lines
在处理器和主存储器之间交换。cache line
是在主存和高速缓存系统之间交换的 64 字节内存块。每个内核都有自己所需的cache line
的副本,这意味着硬件使用值语义。这就是为什么多线程应用程序中内存的变化会造成性能噩梦。
当并行运行的多个线程正在访问相同的数据值,甚至是相邻的数据值时,它们将访问同一cache line
上的数据。在任何核心上运行的任何线程都将获得同一cache line
的副本。
如果某个核心上的一个线程对其cache line
的副本进行了更改,那么同一cache line
的所有其他副本都必须标记为dirty
的。当线程尝试对dirty cache line
进行读写访问时,需要向主存访问(大约 100 到 300 个时钟周期)来获得cache line
的新副本。
也许在一个 2 核处理器上这不是什么大问题,但是如果一个 32 核处理器在同一cache line
上同时运行 32 个线程来访问和改变数据,那会发生什么?如果一个系统有两个物理处理器,每个处理器有16个核心,那又该怎么办呢?这将变得更糟,因为处理器到处理器的通信延迟更大。应用程序将会在主存中周转,性能将会大幅下降。
这被称为缓存一致性问题,还引入了错误共享等问题。在编写可能会改变共享状态的多线程应用程序时,必须考虑缓存系统。
调度决策场景
假设我要求你基于我给你的信息编写操作系统调度器。考虑一下这个你必须考虑的情况。记住,这是调度程序在做出调度决策时必须考虑的许多有趣的事情之一。
启动应用程序,创建主线程并在核心1
上执行。当线程开始执行其指令时,由于需要数据,正在检索cache line
。现在,线程决定为一些并发处理创建一个新线程。下面是问题:
进行上下文切换,切出
核心1
的主线程,切入新线程?这样做有助于提高性能,因为这个新线程需要的相同部分的数据很可能已经被缓存。但主线程没有得到它的全部时间片。新线程等待
核心1
在主线程完成之前变为可用?线程没有运行,但一旦启动,获取数据的延迟将被消除。线程等待下一个可用的核心?这意味着所选核心的
cache line
将被刷新、检索和复制,从而导致延迟。然而,线程将启动得更快,主线程可以完成它的时间片。
有意思吗?这些是系统调度器在做出调度决策时需要考虑的有趣问题。幸运的是,不是我做的。我能告诉你的就是,如果有一个空闲核心,它将被使用。你希望线程在可以运行时运行。
结论
本文的第一部分深入介绍了在编写多线程应用程序时需要考虑的关于线程和系统调度器的问题。这些是 Go 调度器也要考虑的事情。在下一篇文章中,我将解析 Go 调度器的语义以及它们如何与这些信息相关联,并通过一些示例程序来展示。
干货来了!!!为了让更多的小伙伴喜欢Golang、加入Golang之中来,Golang语言社区发起人彬哥联合业界大牛共同推出了Go语言实战系列课程,目前已在网易云课堂上线,希望有兴趣的朋友们多多分享和支持!如果对视频好的建议可加群:713828896,提意见就送社区免费送论坛VIP!
今日起,凡购买网易云课堂课程的付费学员,将有资格参与
社区海外游戏项目的实战开发,名额有限,下周三截止!
长按扫描下方二维码或点击阅读原文,即可了解课程详情!
课程代码和PPT详见 https://github.com/Golangltd/codeclass
以上是关于从Golang调度器的作者视角探究其设计之道!的主要内容,如果未能解决你的问题,请参考以下文章
GO的并发之道-Goroutine调度原理&Channel详解