Golang Go语言中MPG模型是否意味着无论开多少个Goroutine都由核心数个内核进程执行?

Golang Go语言中MPG模型是否意味着无论开多少个Goroutine都由核心数个内核进程执行?

从而不需要像 C/C++那样手动取得 CPU 数 X, 再开 X 个线程去执行任务?

换句话说,这样的代码在 Go 中是不是完全无意义?

CORE_NUM := runtime.NumCPU()   //取得 CPU 数

for i := 0; i < CORE_NUM-1; i++ { //开 CPU-1 个 Goroutine go func() { dojob() } }

dojob() //main 自己的 Goroutine


更多关于Golang Go语言中MPG模型是否意味着无论开多少个Goroutine都由核心数个内核进程执行?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

21 回复

Goroutine 会被分配到若干的线程里运行,线程的数量和运行的核心都是不能控制的。上面的代码确实是没有意义的,Goroutine 是很轻量级的,运行成千上万个都是可以的,没有必要和内核数量关联起来。

更多关于Golang Go语言中MPG模型是否意味着无论开多少个Goroutine都由核心数个内核进程执行?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html



但是比如我需要做 10 万条计算, 我不可能开 10 万个 Goroutine 吧
这时候怎么决定我需要开多少个 Goroutine 呢( and, 每个 Goroutine 分担多少条计算)

好像可以

我理解的,一个 goroutine 默认占用 4K, 那 10 万个占用内存不到半个 G,应该是没啥问题的吧



即使 10 万可以 那 100 万,1000 万, 1 billion 呢?
希望从原理上更好的理解这个问题

cpu 密集型?上 worker pool,起几百上千都行
io 密集型?参考 net/http 和 grpc-go,一个 connection 一个或者两个 goroutine

#2 go 设计 Goroutine 就是为了可以开超多量而设计的,别说 10 万个,百万个千万个页没啥问题,单个的内存消耗很低,但是吧每个 cpu 核心同时刻只能执行一个计算,而且 Goroutine 可能不是抢断式的

#5 线程不能开超多的问题在于每个新线程至少需要 1M 的栈,还有其他一些的内存消耗,受限内存限制所以不能开很多,而且线程是系统切换调度的,超过执行时间就要切换调度,频繁唤醒切换可能需要消耗非常多的 cpu 时间,go 的 Goroutine 单个内存消耗和切换调度消耗都低非常多,所以可以开非常非常多



在使用线程实现并发的语言里,像你问题里提到按照内核设置线程数,目的是期望每个线程都跑在一个独立内核上,减少不停进行上下文切换导致的额外开销。而 goroutine 是 go 的 runtime 自己调度的,并且比线程轻量级,带来两个好处:调度开销小和默认内存小。所以 goroutine 不再有之前线程出于性能考虑而带来的数量限制,所以理论上只要内存和 cpu 够强,开多少都行。

总的来说,启动线程考虑数量和内核的关系,是因为可能存在的性能问题带来的限制。而 goroutine 去掉了这个限制。你的问题像是一个之前被约束的人,有了自由后突然之间无所适从了。

如果你开始用 go 了,可以尝试习惯从业务和系统的总体计算力里来考虑并发数量,不用再把数量带来的上下文交换带来的消耗和内存占用放在考虑范围内了。

go 的优秀之处就是随意开 goroutine 了,而且自带线程调度,写并发程序的心智负担降到最低
goroutine 运行完成会回收的,如果有什么项目要开到上亿且不释放,他应该考虑分布式计算


一个 CPU connection 一个或者两个 goroutine? 为什么?


看下这个简单的 worker pool
’’‘
package main

import (
“fmt”
“time”
)
//睡 1 秒,然后把收到的数据乘 2 返回
func worker(id int, job <-chan int, ret chan<- int) {
for j := range job {
time.Sleep(time.Second)
ret <- j * 2
}
}



func main() {
//1000 个工作
const JOBNUM = 1000
jobchan := make(chan int, JOBNUM)
retchan := make(chan int, JOBNUM)

//开 1000 个 goroutine
for w := 1; w <= 1000; w++ {
go worker(w, jobchan, retchan)
}

//分发 1000 个工作
for j := 1; j <= JOBNUM; j++ {
jobchan <- j
}
close(jobchan)


//接受结果
for r := 1; r <= JOBNUM; r++ {
<-retchan
}
}
’’'
如果说"每个 cpu 核心同时刻只能执行一个计算"
我的电脑有 12 核心。那么按理说我同时只能处理 12 个 goroutine,即 12 个 worker 不是吗
可实际上这 1000 个 goroutine 处理 1000 个工作(睡 1 秒),
总共只用了 1 秒, 说明这 1000 个 goroutine 是同时并行执行的
而不是每次并行处理 12 个,一共要睡( 1000/12 )= 83 秒

肯定是我哪里理解不对 望指点

sleep 不需要实际上阻塞等待那一段时间,可以让调度器在给定时间后调度就好了
然后调度器发现所有 goroutine 都在 sleep 的时候才会 sleep


每次并行处理 12 个!=每秒并行 12 个。
sleep 的时候,Go 是会让出资源的。

#11 这个 sleep 和普通 c 用的 sleep 不是同一个,这个 sleep 是实现在 go 的 goroutine 调度器里的,不是阻塞的,调用 sleep 的时候会进行 goroutine 调度,切换到其他 goroutine 运行,时间到了再进入调度再运行,线程调用 sleep 不也不会阻塞 cpu 运行啊,这和线程是一样的

你电脑又不是单线程的,一个和核心也可以调度运行多个线程呀
你测试的代码说明不了啥,你开 1000 个线程也是一样的效果
你这里用的 sleep,cpu 并不需要傻等,直接调度执行别的线程就行
goroutine 的实际运行需要依附于一个具体的线程,也就是 gmp 里面的 M
决定同时有多少个 goroutine 可以执行是 P 的数量决定的
你可以看成 go 给你抽象出来了一个有 P 个核心的机器,P 在调度运行 goroutine 的时候,会把 goroutine 附加到一个实际的 M 上
如果 goroutine 发生系统调用陷入内核态了,这个时候 M 就释放不了,P 也会自己创建一个新线程来调度别的 G 来执行

实际上还是有用的哦,如果 goroutine 里有阻塞操作,那么 M 被占用完了,没得调度之后会新开 M,也就是新的 OS 线程。

会导致大量线程出现,影响效率。所以有些情况你得限制你的 goroutine 数量。

大部分情况是这样,特例有 cgo,如果在 go 里面调用了 c 的代码,使用 ps -eLf 观察会发现很多内核线程。

如果是长时间纯数据处理的程序, 可以开 CPU 个 routine. 这样每个核抢到一个 routine. 然后用 channel 来塞数据. 减少了 routine 切换的性能影响.

goroutine 进行阻塞调用比如 sleep 或者 socket 读取并不回阻塞对应的 cpu 核心,go 的 runtime 会把阻塞的 goroutine 切换走置于休眠态,一旦解除阻塞的条件达成,如 sleep 时间到或者 socket 有可读数据,则会唤醒相应的 goroutine,这个过程跟操作系统调度进程很像,所以可以认为 goroutine 就是一个用户态线程实现。回到你的问题,答案是:cpu 有多少个核心跟启动多少个 goroutine 没关系,因为 go 的 runtime 会自动切换走处于阻塞的 goroutine,从而来充分利用 cpu 资源。但是有一个原则是通用的,cpu 密集型的任务尽量启用跟 cpu 核数差不多的 worker,因为在 cpu 密集型任务里面 cpu 一直处于饱和计算状态,无谓的上下文切换反而影响效率。

实际是有用的 https://github.com/uber-go/automaxprocs 这个包专门为容器内的应用设计 就是为了防止 runtime 获取到错误的核心数,开太多的 P

作为IT领域GO语言方面的专家,对于Golang中的MPG模型有深入了解,以下是对该问题的专业解答:

MPG模型是Golang并发编程的核心,由M(Machine,内核线程)、P(Processor,处理器)、G(Goroutine,协程)三部分组成。其中,M代表内核线程,负责执行计算任务;P代表逻辑处理器,是Goroutine执行的实际载体,每个P都会维护一个可运行的Goroutine队列;G代表Goroutine,是轻量级的用户线程,由Go运行时调度和管理。

在Golang程序中,Goroutine的数量可以非常庞大,但真正同时执行Goroutine的内核线程(M)数量是有限的,这受限于系统的核心数以及Go运行时的配置。Go运行时会自动调度Goroutines,在多个M上执行,使得它们看起来像是同时运行的。

因此,可以说在Golang中,无论开多少个Goroutine,它们最终都是由核心数个内核进程(更准确地说是内核线程M)来并发或并行执行的。但得益于Go的高效调度和Goroutine的轻量级特性,我们可以实现高并发编程,同时避免过多的上下文切换和资源消耗。

回到顶部