阅读Go并发编程对go语言线程模型的笔记,解释的非常到,好记性不如烂笔头,忘记的时候回来翻一番,在此做下笔记。
Go语言的线程实现模型,又3个必知的核心元素,他们支撑起了这个线程实现模型的主要框架:
1>M:Machine的缩写。一个M代表一个内核线程。
2>P:Procecssor的缩写。一个P代表了M所在的上下文环境。
3>G:Goroutine的缩写。一个G代表了对一段需要被并发执行的Go语言代码的封装。
简单的来说,一个G的执行文件需要M和P的支持。一个M在与一个P关联形成一个有效的G运行环境(内核线程+上下文环境)。
每个P都会包含一个可运行的G的队列(runq)。该队列的G会被依次传给与本地P关联的M并获得运行时机。在这里,
我们把运行当前程序的那个M称为当前M,而把与当前M关联的那个P称为本地P。
M(Machine)与KSE(Kernel Schedule Entity)之间总一对一的。一个M能且仅代表一个内核线程。Go语言的运行
时系统(runtime system)用它来代表一个内核调度系统。
0|11.M(Machine)
一个M代表了一个内核线程。大多数情况下,创建一个M的原因都是由于没有足够的M来关联P(Process)
并运行其中的可运行的G。不过,在运行时系统执行系统监控或垃圾回收等任务的时候也会
导致新的M的创建。M(Machine)的数据结构包括(curg p mstartfn nextp)。
M(Machine)结构中的字段众多。我们在这里只是挑选了对于我们的初步认识M(Machine)最重要的4个字段。其中字段
curg会存放当前M正在运行的那个G(goroutine)的指针,字段p会指向与当前M相关联的那个P,而字段mstartfm则代表
我们马上会讲到的M(Machine)的起始函数。在M被调度的过程中,这三个字段最能体现他的即使情况。而另外的字段nextp则
会被用于暂存与当前M(Machine)又潜在关系的P。我们可以把调度器将某个P(Process)赋值给某个M的nextp字段的操作称为
M和P的预联。在有些时候,运行时系统给会把刚刚被重启新启用的M(Machine)和它预联的那个P关联在一起,这就是nextp字段的所起到的作用。
M被创建之初会被加入全局的M(Machine)列表(runtie.allm)中。紧接着,它的起始函数和准备关联的P(Process)(大多数
情况下导致次M(Machine)创建操作的那个P(Process))会被设置。最后,运行时系统会为它专门创建一个新的内核线程并与之
关联。这样,这个新的M(Machine)就为执行G(Goroutine)做好了准备。而这里的全局M(Machine)列表其实并没有什么特殊的意义。
运行时系统在需要的时候会通过它获取所有M的信息。同时它也防止M被当作垃圾回收。
在新的M被创建完成之后的会先进行一番初始化工作。其中包括了对自身所持的栈空间以及信号处理方面的初始化。
在这些初始化工作都完成之后。该M将会被执行(如果存在的话)。注意,如果在这个起始函数代表的是系统监控的任务
的话,那么该M会一直在那里执行而不会继续后面的流程。否则,在初始函数被执行完毕后。当前M将会与那个准备与
它关联的P完成关联。至此,一个并发执行环境才真正的形成。在这之后,M开始寻找可运行的G并运行它,这一过程
可以被看做是调度的一部分。
运行时系统所管辖的M(或者说runtime.allm中的M)有时候会被停止,比如在运行时系统准备开始执行垃圾回收任务时候。
运行时系统停止在M的时候,会对它的属性进行必要的重置之后,把它放进调度器的空闲M列表(runtime.sched.midle)。
因为在需要一个未被使用的M的时候,运行时系统会尝试从该列表中。
注意,M本身是无状态的。M是否空闲仅仅以为它是否存在于调度器的空闲M列表中为依据。虽然运行时系统可以通过M列表
获取所有的M,但是却无法得知它们的状态(因为它们没有状态)。
单个Go程序所使用的M最大数据是可以被设置的。在我们使用命令运行Go程序的时候,一个引导程序先会被启动。
这个引导程序先会被启动,这个初始值是1w。也就是说,一个Go程序最多可以使用1w个M。
这就以为着。在最理想的情况下,同时可以有1w个内核线程同时被执行。请注意,这里说的是最理想的i情况下的。
由于操作系统的内核对进程的虚拟内存的布局的控制以及大小的限制,如此量级的线程很难共存。从这个角度看。
Go语言本身对线程的线程数量几乎可以被忽略。
出了上述设置外,我们也可以在Go程序中对该限制进行设置。为了达到此目的,我们需要调用标准库的代码包runtime/debug包
中的SetMaxThreads函数并且对提供新的M最大数量。runtime/debug.SetMaxThreads函数在执行后,会把旧的M最大数量作为结果
值返回。非常重要的一点是,如果我嫩在调用runtime/debug.seMaxThreads函数时给定的新值比当时M的实际数量还要小的话,
运行时系统就会发起一个运行时恐慌。所以,我们要小心使用这个函数。请记住,如果我们真的需要设置M的最大数量。
那么也早调用runtime/debug.SetMaxThreads函数就也好,对于它的设定值,我们也要仔细斟酌。
0|12.P(Process)
P(Process)是使G能够在M中运行的关键。Golang的运行时系统会实时地让P与不同的M建立或断开关联,以使P中的那些可运行的
G能够在需要的时候及时获得运行时机。这与操作系统内核在CPU之上切换不同的进程或者线程类似。
通过调用函数runtime.GOMAXPROCS,我们可以改变单个Go程序可以间接拥有的P的最大数量。初除此自外,我们还可以在运行Go程序
之前设置环境变量GOMAXPROCS的值对Go程序的可以用的P最大的数量做出预先设定。P的最大数量相当于是对可以被并发运行的用户
级别的G的数量做出限制。我们已经知道,每个P都需要关联一个M(Machine)才能使其中的可运行的G得到执行。但是这却不意味着
环境变量GOMAXPROCS的值会被限制住M的总数量。当M因系统调用的进行而被阻塞(更切确的说,是它运行的G进入了系统的调用)的
时候,运行时系统会将该M和与之关联的P分离出来。这时,如果这个P的可运行G队列中还未被运行的G,那么运行时系统
就会找到一个空闲M,或创建出一个新的M,并与该P关联以满足这些G运行需要。如果我们在Go程序中创建大部分Goroutine中
都包含了很多需要的间接地进行各种系统调用(比如各种I/O操作)代码的话,那么即使环境变量GOMAXPROCS的值被设定未1,也
很可能被创建很多个M被创建出来。所以,实际的M总数量很可能比环境变量GOMAXPROCS所指代的数量多。由此可见,Go程序
真正使用的内核线程的数量并不会因此而受到限制。
在Go程序开始被运行的时候,我们在前面提到的引导程序也会对P的最大数量进行设置。P的最大数量的默认值是1。因此。
在默认的情况下,无论我们在程序中用go语句启用多个Goroutine。它们都只会被塞入同一个P的可运行G的队列中,当
环境变量GOMAXPROCS的值的有效就会被这个硬性限制取代,也就是说,最终的P最大数量值绝对不会比引导程序中的这个硬性
上线值打。该硬性上限值是2的8次方。即256.这个硬性上限值为256的原因是Go语言目前还不能保证在数量比256更多的P同时存在的
情况下Go程序仍能保持高效。也就是说,这个硬行上线并不是永久的,它在以后可能会被改变
[
runtime.GOMAXPROCS函数减少P最大数量的时候,其余的P就会被运行时系统置于此状态。P的初始状态是Pgcstop,
虽然运行时系统并不会再这时进行垃圾回收。不过,P处于这一初始状态的时间会非常短暂。紧接着的初始化和填充P中的可
运行G队列之后,运行时系统会被其状态设置未Pidle并放入到调度器的空闲列表中。此空闲P列表中的所有P都有调度器根据实际
情况经进行取用。
