golang是如何实现高并发的?深入领会MPG模式

前天去面试,被问到golang是如何实现高并发的,之前在 GO并发编程实战 这本书看到过介绍,但是没有引起重视。
传统的并发形式:多线程共享内存,这也是Java、C#或者C++等语言中的多线程开发的常规方法,其实golang语言也支持这种传统模式,另外一种是Go语言特有的,也是Go语言推荐的:CSP(communicating sequential processes)并发模型。不同于传统的多线程通过共享内存来通信,CSP讲究的是“以通信的方式来共享内存”。
不要以共享内存的方式来通信,相反,要通过通信来共享内存。

go语言使用MPG模式来实现CSP
在传统的并发中起很多线程只会加大CPU和内存的开销,太多的线程会大量的消耗计算机硬件资源,造成并发量的瓶颈。

M指的是Machine,一个M直接关联了一个内核线程。
P指的是”processor”,代表了M所需的上下文环境,也是处理用户级代码逻辑的处理器。
G指的是Goroutine(协程),其实本质上也是一种轻量级的线程。

三者关系如下图所示:

我个人的理解:M关联了一个内核线程,通过调度器P(上下文)的调度,可以连接1个或者多个G,相当于把一个内核线程切分成了了N个用户线程,M和P是一对一关系(但是实际调度中关系多变),通过P调度N个G(P和G是一对多关系),实现内核线程和G的多对多关系(M:N),通过这个方式,一个内核线程就可以起N个Goroutine(协程),同样硬件配置的机器可用的用户线程就成几何级增长,并发性大幅提高。
这篇文章详细介绍了相关原理:
https://i6448038.github.io/2017/12/04/golang-concurrency-principle/

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

  1. 创建一个G对象,优先加入到本地队列(每个P最多256个),如果本地队列满了就会加入全局队列。
  2. 如果还有空闲的P,则创建一个M
  3. M会启动一个底层线程,循环执行能找到的G任务
  4. G任务的执行顺序是,先从本地队列找,本地没有则从全局队列找(一次性转移(
    全局G个数/P个数)个,再去其它P中找(一次性转移一半),

以上的G任务执行是按照队列顺序(也就是go调用的顺序)执行的。

这篇文章《go语言的并发原理(goroutine)》介绍的很详细
https://blog.csdn.net/ydl1128/article/details/126171448

goroutine调度
goroutine 的调度是Go语言运行时(runtime)层面的实现,是完全由 Go 语言本身实现的一套调度系统——go scheduler。它的作用是按照一定的规则将所有的 goroutine 调度到操作系统线程上执行。

在经历数个版本的迭代之后,目前 Go 语言的调度器采用的是 GPM 调度模型

在这里插入图片描述

其中:

G:表示 goroutine,每执行一次go f()就创建一个 G,包含要执行的函数和上下文信息。

全局队列(Global Queue):存放等待运行的 G

P:表示 goroutine 执行所需的资源,最多有 GOMAXPROCS 个,在golang中,可以通过设置GOMAXPROCS环境变量来设置CPU核心数(即设置P的数量)。

P 的本地队列:同全局队列类似,存放的也是等待运行的G,存的数量有限,不超过256个新建 G 时,G 优先加入到 P 的本地队列,如果本地队列满了会批量移动部分 G 到全局队列。

M:线程想运行任务就得获取 P,从 P 的本地队列获取 G,当 P 的本地队列为空时,M 也会尝试从全局队列或其他 P 的本地队列获取 G。M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去。

Goroutine 调度器和操作系统调度器是通过 M 结合起来的,每个 M 都代表了1个内核线程,操作系统调度器负责把内核线程分配到 CPU 的核上执行。

单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的goroutine 则是由Go运行时(runtime)自己的调度器调度的,完全是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身 goroutine 的超轻量级,以上种种特性保证了 goroutine 调度方面的性能。

M:N:把m个goroutine分配给n个操作系统线程去执行
goroutine的初始栈大小是2K,能够轻松建立上万个goroutine
而线程的初始栈内存高达2M,明显比goroutine大的多

  • 6
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Golang的并发编程使用goroutine和channel实现。Goroutine是一种轻量级的线程,可以在同一进程内并发地运行多个任务,而不会造成线程上下文切换的开销。Channel是一种特殊的数据类型,用于在不同的goroutine之间传递数据。 使用goroutine非常简单,只需要在函数调用前加上关键字go即可启动一个goroutine。例如: ``` func main() { go func() { // 这里是一个新的goroutine }() // 这里是主goroutine } ``` 使用channel也很简单,可以使用make函数创建一个channel,然后使用<-操作符发送或接收数据。例如: ``` ch := make(chan int) go func() { ch <- 42 // 发送数据 }() x := <-ch // 接收数据 ``` 在Golang,channel的发送和接收操作是阻塞的。当发送者向一个已满的channel发送数据时,发送操作会被阻塞,直到有接收者从该channel接收数据。同样地,当接收者从一个空的channel接收数据时,接收操作也会被阻塞,直到有发送者向该channel发送数据。 除了基本的发送和接收操作外,channel还支持以下操作: - 关闭通道:使用close函数关闭通道。关闭通道后,无法再发送数据,但可以继续接收数据。 - 判断通道是否已关闭:使用_, ok := <-ch语句判断通道是否已关闭。 - 使用带缓冲的通道:使用make函数创建一个带缓冲的通道,例如ch := make(chan int, 10)。带缓冲的通道可以存储一定数量的数据,当缓冲区已满时,发送操作会阻塞,直到有接收者从该通道接收数据。 通过goroutine和channel的组合,Golang实现了高效的并发编程,这也是Golang的一大特色。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值