Go 语言的 MPG 模型是不是意味着无论我开多少个 Goroutine,最后都是由核心数个内核进程去执行?

2020-12-04 10:02:27 +08:00
 b00tyhunt3r

从而不需要像 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
3159 次点击
所在节点    Go 编程语言
20 条回复
zmqiang
2020-12-04 10:05:54 +08:00
Goroutine 会被分配到若干的线程里运行,线程的数量和运行的核心都是不能控制的。上面的代码确实是没有意义的,Goroutine 是很轻量级的,运行成千上万个都是可以的,没有必要和内核数量关联起来。
b00tyhunt3r
2020-12-04 10:11:23 +08:00
@zmqiang
但是比如我需要做 10 万条计算, 我不可能开 10 万个 Goroutine 吧
这时候怎么决定我需要开多少个 Goroutine 呢( and, 每个 Goroutine 分担多少条计算)
lithium4010
2020-12-04 10:15:21 +08:00
@b00tyhunt3r 好像可以
bruce0
2020-12-04 10:16:16 +08:00
@b00tyhunt3r 我理解的,一个 goroutine 默认占用 4K, 那 10 万个占用内存不到半个 G,应该是没啥问题的吧
b00tyhunt3r
2020-12-04 10:19:08 +08:00
@lithium4010
@bruce0
即使 10 万可以 那 100 万,1000 万, 1 billion 呢?
希望从原理上更好的理解这个问题
axex
2020-12-04 10:21:46 +08:00
cpu 密集型?上 worker pool,起几百上千都行
io 密集型?参考 net/http 和 grpc-go,一个 connection 一个或者两个 goroutine
sujin190
2020-12-04 10:21:50 +08:00
@b00tyhunt3r #2 go 设计 Goroutine 就是为了可以开超多量而设计的,别说 10 万个,百万个千万个页没啥问题,单个的内存消耗很低,但是吧每个 cpu 核心同时刻只能执行一个计算,而且 Goroutine 可能不是抢断式的
sujin190
2020-12-04 10:27:28 +08:00
@b00tyhunt3r #5 线程不能开超多的问题在于每个新线程至少需要 1M 的栈,还有其他一些的内存消耗,受限内存限制所以不能开很多,而且线程是系统切换调度的,超过执行时间就要切换调度,频繁唤醒切换可能需要消耗非常多的 cpu 时间,go 的 Goroutine 单个内存消耗和切换调度消耗都低非常多,所以可以开非常非常多
zmqiang
2020-12-04 10:30:27 +08:00
@b00tyhunt3r

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

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

如果你开始用 go 了,可以尝试习惯从业务和系统的总体计算力里来考虑并发数量,不用再把数量带来的上下文交换带来的消耗和内存占用放在考虑范围内了。
wangritian
2020-12-04 10:32:18 +08:00
go 的优秀之处就是随意开 goroutine 了,而且自带线程调度,写并发程序的心智负担降到最低
goroutine 运行完成会回收的,如果有什么项目要开到上亿且不释放,他应该考虑分布式计算
b00tyhunt3r
2020-12-04 11:10:19 +08:00
@axex
一个 CPU connection 一个或者两个 goroutine? 为什么?

@sujin190
看下这个简单的 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 秒

肯定是我哪里理解不对 望指点
codehz
2020-12-04 11:33:35 +08:00
sleep 不需要实际上阻塞等待那一段时间,可以让调度器在给定时间后调度就好了
然后调度器发现所有 goroutine 都在 sleep 的时候才会 sleep
6IbA2bj5ip3tK49j
2020-12-04 11:33:43 +08:00
@b00tyhunt3r
每次并行处理 12 个!=每秒并行 12 个。
sleep 的时候,Go 是会让出资源的。
sujin190
2020-12-04 11:34:27 +08:00
@b00tyhunt3r #11 这个 sleep 和普通 c 用的 sleep 不是同一个,这个 sleep 是实现在 go 的 goroutine 调度器里的,不是阻塞的,调用 sleep 的时候会进行 goroutine 调度,切换到其他 goroutine 运行,时间到了再进入调度再运行,线程调用 sleep 不也不会阻塞 cpu 运行啊,这和线程是一样的
unixeno
2020-12-04 11:42:05 +08:00
@b00tyhunt3r 你电脑又不是单线程的,一个和核心也可以调度运行多个线程呀
你测试的代码说明不了啥,你开 1000 个线程也是一样的效果
你这里用的 sleep,cpu 并不需要傻等,直接调度执行别的线程就行
goroutine 的实际运行需要依附于一个具体的线程,也就是 gmp 里面的 M
决定同时有多少个 goroutine 可以执行是 P 的数量决定的
你可以看成 go 给你抽象出来了一个有 P 个核心的机器,P 在调度运行 goroutine 的时候,会把 goroutine 附加到一个实际的 M 上
如果 goroutine 发生系统调用陷入内核态了,这个时候 M 就释放不了,P 也会自己创建一个新线程来调度别的 G 来执行
rrfeng
2020-12-04 12:27:10 +08:00
实际上还是有用的哦,如果 goroutine 里有阻塞操作,那么 M 被占用完了,没得调度之后会新开 M,也就是新的 OS 线程。

会导致大量线程出现,影响效率。所以有些情况你得限制你的 goroutine 数量。
guonaihong
2020-12-04 13:12:01 +08:00
大部分情况是这样,特例有 cgo,如果在 go 里面调用了 c 的代码,使用 ps -eLf 观察会发现很多内核线程。
joesonw
2020-12-04 15:27:11 +08:00
如果是长时间纯数据处理的程序, 可以开 CPU 个 routine. 这样每个核抢到一个 routine. 然后用 channel 来塞数据. 减少了 routine 切换的性能影响.
icexin
2020-12-04 15:28:38 +08:00
goroutine 进行阻塞调用比如 sleep 或者 socket 读取并不回阻塞对应的 cpu 核心,go 的 runtime 会把阻塞的 goroutine 切换走置于休眠态,一旦解除阻塞的条件达成,如 sleep 时间到或者 socket 有可读数据,则会唤醒相应的 goroutine,这个过程跟操作系统调度进程很像,所以可以认为 goroutine 就是一个用户态线程实现。回到你的问题,答案是:cpu 有多少个核心跟启动多少个 goroutine 没关系,因为 go 的 runtime 会自动切换走处于阻塞的 goroutine,从而来充分利用 cpu 资源。但是有一个原则是通用的,cpu 密集型的任务尽量启用跟 cpu 核数差不多的 worker,因为在 cpu 密集型任务里面 cpu 一直处于饱和计算状态,无谓的上下文切换反而影响效率。
songjiaxin2008
2020-12-04 15:31:01 +08:00
实际是有用的 https://github.com/uber-go/automaxprocs 这个包专门为容器内的应用设计 就是为了防止 runtime 获取到错误的核心数,开太多的 P

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/731996

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX