在学习Go语言内存管理部分过程中,发现了一个很有意思的问题,今天就借助这篇文章:
- 1.把这个问题也抛给大家,建议大家看见这个问题后,可以先自己思考一番之后再读下文。
- 2.进一步强化大家对Go内存架构的理解
开始本篇文章之前,我们快速回顾下「Go内存架构」相关的核心知识点,温故知新。
快速回顾「TCMalloc内存管理架构」
先来简单回顾下「TCMalloc内存管理架构」。详细讲解可查看之前的文章《18张图解密新时代内存分配器TCMalloc》
痛点
多线程时代 --->
线程共享内存 --->
线程申请内存会产生竞争 --->
竞争加锁 --->
加锁影响性能。
解法
每个线程上增加内存缓存。
简易架构图如下:
快速回顾「Go内存管理架构」
接着简单回顾下「Go内存管理架构」。详细讲解可查看之前的文章《浅析Go内存管理架构》
痛点
同上。
解法
同上,基于「TCMalloc」实现。
简易架构图如下:
有趣的问题
关于这个有趣的问题,细心的朋友可能已经发现了,不卖关子了问题如下:
为什么Go的内存管理器的线程缓存是mcache
被逻辑处理器p
持有,而并不是被真正的系统线程m
持有?
个人思考时间
是不是很有意思,关于这个问题。对面的你不妨先停下来思考几分钟:
为什么?
解密
按照原TCMalloc
的设计思想,线程缓存mcache
确实应该被绑定到系统线程M
上。
那么我们就假设:按照原TCMalloc
的思想,把mcache
绑定系统线程M
上。接着我们只需要看看这个假设有什么问题即可。
要论证这个假设需要先来简单看看「Go的调度模型GMP
」。
Go的调度模型GMP
直接上入门级「Go的调度模型GMP
」架构图:
关于「Go的调度模型GMP
」的原理,大家应该看了无数文章,我这里就不细说了,如果还有不熟悉可以自行搜索哈。
这里简单提下关于GMP
的入门级知识哈,其实GMP
对应的只是Go语言自身的逻辑结构而已,含义如下:
M
:代表结构体m
,全称Machine
,这个结构体的核心是会和真正的系统线程thread
绑定。G
:代表结构体g
,全称Goroutine
,这个结构体就是大家熟知的协程
,简单理解其实就是这个结构体绑定了一个有着被并发执行需求的函数。P
:代表结构体p
,全称Processor
,这个结构体表示逻辑处理器
,通过这个结构体和计算机的逻辑处理器建立对应关系,P
的数量通常和计算机的逻辑处理器数量一致通过runtime.GOMAXPROCS(runtime.NumCPU())
设置。
三者的简单职责以及关系:
P
- 和一个
M
互相绑定 - 维护了一个可执行
G
的队列
- 和一个
M
- 和一个
P
互相绑定 - 负责执行
G
的调度,通过调度当前M
绑定的P
的G
队列、以及全局G
队列,达到G
可被并发执行的目的。 - 负责执行
P
调度过来的当前G
- 和一个
此阶段结论:以上的调度过程P
的数量和M
的数量是一一对应的,所以把mcache
绑定系统线程M
上和P
看起来都可以。所以我们上面的假设「按照原TCMalloc
的思想,把mcache
绑定系统线程M
上」目前看起来确实也没啥问题。
我们继续往下看,一种特殊的场景M
会和P
解绑。
I/O操作的系统调用
当G
执行一个I/O操作的系统调用时,比如read
、write
,因为系统调用过程中的阻塞(原因:内核往用户态拷贝数据的过程产生的阻塞,不在本文范畴,后续文章详解)问题,会发生如下操作:
- 当前
G
(我们命名为g1
)的M
(我们命名为m1
)和当前的P
(我们命名为p1
)解绑 - 上面的
p1
会绑定一个其他的M
(m2
) m1
执行完成系统调用之后会被放到闲置M
链表里
由于m1
会被放进闲置链表,这是不是就意味着m1
上的mcache
当前就不能被复用,所以这样看起来是不是mcache
绑定到p1
上更合适。
结论: 由于M
可能因为执行一个I/O操作的系统调用被阻塞(原因:内核往用户态拷贝数据的过程产生的阻塞),M
会和当前P
解绑,当前P
绑定其他闲置或者新的M
,之前的M
结束系统调用会被放进闲置M
链表。之前的M
的mcache
就不会得到有效的复用,反而mcache
绑定到P
上就不存在这个问题,所以mcache
绑定到P
上更合适。
查看《Go语言轻松进阶》系列更多内容
链接 http://tigerb.cn/go/#/kernal/