golang的goroutine调度机制

一直goroutine度机制很好奇,最近在看雨痕的golang分析,(基于go1.4)

豁然开朗,受益匪浅;

去繁就,再加上自己的一些理解,整理了一下

~~

调度

主要基于三个基本象上,GMP(定src/runtime/runtime.h文件中)

1.     G代表一个goroutine对象,每次go调用的时候,都会创建一个G

2.     M代表一个线程,每次建一个M候,都会有一个底层线建;所有的G,最终还是在M

3.     P代表一个理器,每一个运行的M都必须绑定一个P,就像线程必在么一个CPU核上行一

P的个数就是GOMAXPROCS(最大256),启动时固定的,一般不修改; M的个数和P的个数不一定一样多(会有休眠的M或者不需要太多的M)(最大10000);每一个P保存着本地G任务队列,也有一个全局G任务队列;

如下图所示


全局G任务队列会和各个本地G任务队列按照一定的策略互相交换(满了,则把本地队列的一半送给全局队列)

P是用一个全局数组(255)来保存的,并且维护着一个全局的P空闲链表

每次go调用的时候,都会:

1.     创建一个G对象,加入到本地队列或者全局队

2.     如果有空P则创建一个M

3.     M会启一个底层线程,循环执行能找到的G

4.     G序是,先从本地列找,本地没有从全局列找(一次性(全局G个数/P个数)个,再去其它P中找(一次性移一半),

5.     以上的G务执行是按照序(也就是go调用的顺序)执行的。(这个地方是不是觉得很奇怪??

对于上面的第2-3步,创建一个M,其过程:

1.     先找到一个空P,如果没有直接返回,(哈哈,个地方就保程不会占用超自己定的cpu个数)

2.     调用系统api创建线程,不同的操作系统,调用不一样,其实就是和c语言创建过程是一致的,(windows用的是CreateThreadlinux用的是clone统调用),(*^__^*)嘻嘻……

3.     然后建的线程里面才是真正做事的,循环执G

那就会有个问题,如果一个系统调用或者G任务执行太长,他就会一直占用这个线程,由于本地队列的G任务是顺序执行的,其它G任务就会阻塞了,怎样中止长任务的呢?(这个地方我找了好久~o()o

这样滴,启动的时候,会专门创建一个线程sysmon,用来监控和管理,在内部是一个循环:

1.     记录所有PG务计schedtick,(schedtick会在每行一个G增)

2.     如果检查 schedtick一直没有增,P一直在行同一个G,如果超一定的时间10ms),就在G信息里面加一个标记

3.     然后G行的候,如果遇到非内函数用,就会检查一次标记,然后中断自己,把自己加到列末尾,行下一个G

4.     O(∩_∩)O哈哈~,如果没有遇到非内函数(有候正常的小函数会被化成内函数)用的,那就惨了,会一直G,直到它自己束;如果是个死循,并且GOMAXPROCS=1,恭喜你,夯住了!亲测,的确如此

对于一个G任务,中断后的恢复过程:

1.     中断的候将寄存器里的栈信息,保存到自己的G对象里

2.     当再次到自己,将自己保存的信息复制到寄存器里面,这样就接着上次之后运行了。 ~\(≧▽≦)/~

 

但是还有一个问题,就是系统启动的过程,雨痕没有说的太明白,我一直有很多问题都狠疑惑(第一个M怎么来的?,G怎么找到对应的P等等),这个让我蛋疼了好久~

不过我自己意淫了一下,补充在下面,欢迎大家指正

1.     ,首先跑的是主线程,那第一个M应该就是线程吧(按照C语言的理解,嘿嘿),这里叫M1,可以看前面的

2.     然后这个主线程第一个P1

3.     写的main函数,其是作一个goroutine行的(雨痕的)

4.     也就是第一个P1就有了一个G1,然后第一个M1G1(也就是main函数),G1候不用M了,因有了M1

5.     这个main函数里面所有的goroutine,都定到当前的M1对应P1上,O(∩_∩)O哈哈~

6.     然后main里的goroutine候(比如G2),就会建新的M2,新的M2里的初始P2的本地任务队列是空的,会从P1里面取一些来,哈哈

7.     这样两个M1M2各自行自己的G,再依次往复,下就圆满~~~

 

综上:

所以goroutine是按照抢占式调度的,一个goroutine最多执行10ms就会换作下一个

这个和目前主流系统的的cpu调度类似(按照时间分片)

windows20ms

linux5ms800ms


到这里都差不多了,这些在雨痕的笔记里面都有更详细的描述,不过很多地方比较凌乱,比较复杂,这里筛检了很多,方便读者理解

 

注意:

1.     Golang编译器也会尝试进行内,将小函数直接复制并编译了内,尽量消除编译器无法侦测dead code,利用gobuild -gcflags=-m编译命令可以查看程序内联状态,不得不说golang编译工具链还是很大的,十分有利于程序的化。

 

如果有任何疑问,欢迎提出,

随时更新


(这篇文章是去年整理的,记录公司内部wiki上~)

### Goroutine 工作机制 在 Go 语言中,Goroutine 是一种轻量级的线程管理机制[^2]。当函数前加上 `go` 关键字时,该函数就会在一个新的 Goroutine 中异步执行。Go 运行时会自动管理和调度这些 Goroutine。 #### 创建和销毁成本低 相比于传统操作系统级别的线程,Goroutine 的创建和销毁代价极小。每个 Goroutine 只需大约 4~5 KB 的栈空间,并且可以根据需要动态调整大小。这种高效的资源利用方式使得可以在单个进程中轻松启动成千上万个 Goroutine 来处理并发任务[^3]。 #### 调度器的作用 Go 运行时内部有一个专门的任务调度器负责分配 CPU 时间给各个活跃的 Goroutine。这个调度器能够根据当前系统的负载情况智能地平衡各 Goroutine 执行时间片,从而保证整体性能最优[^1]。 --- ### 使用方法示例 下面是一个简单的例子展示了如何使用 goroutines 实现基本的并发操作: ```go package main import ( "fmt" "time" ) func say(s string) { for i := 0; i < 5; i++ { time.Sleep(100 * time.Millisecond) fmt.Println(s) } } func main() { go say("world") // 启动一个新的goroutine去执行say函数 say("hello") // 主goroutine继续在这里阻塞直到完成自己的工作 } ``` 在这个例子中,`main()` 函数启动了一个新 goroutine 来打印 "world" 字符串五次的同时自己也打印了 "hello"[^4]。 需要注意的是,在上面的例子中如果希望等待子 goroutine 完成后再退出程序,则可以通过 WaitGroup 或者 Channel 等同步原语来实现这一点。 ---
评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值