我可以毫不费力地使用多少个goroutine呢? 例如,维基百科说,在Erlang中可以创建2000万个进程而不降低性能。
更新: 我刚刚研究了一下goroutine的性能,得出了以下结果:
- 似乎goroutine的生命周期长于计算sqrt() 1000次(对我来说大约是45µs),唯一的限制是内存
- Goroutine的成本为4-4.5 KB
我可以毫不费力地使用多少个goroutine呢? 例如,维基百科说,在Erlang中可以创建2000万个进程而不降低性能。
更新: 我刚刚研究了一下goroutine的性能,得出了以下结果:
如果一个 goroutine 被阻塞了,那么除了以下成本之外,就没有其他成本了:
这些成本(以内存和实际开始执行 goroutine 的平均时间为代价)是:
Go 1.6.2 (April 2016)
32-bit x86 CPU (A10-7850K 4GHz)
| Number of goroutines: 100000
| Per goroutine:
| Memory: 4536.84 bytes
| Time: 1.634248 µs
64-bit x86 CPU (A10-7850K 4GHz)
| Number of goroutines: 100000
| Per goroutine:
| Memory: 4707.92 bytes
| Time: 1.842097 µs
Go release.r60.3 (December 2011)
32-bit x86 CPU (1.6 GHz)
| Number of goroutines: 100000
| Per goroutine:
| Memory: 4243.45 bytes
| Time: 5.815950 µs
源代码(如果您已经理解上面打印的数字,则无需阅读此内容):
package main
import (
"flag"
"fmt"
"os"
"runtime"
"time"
)
var n = flag.Int("n", 1e5, "Number of goroutines to create")
var ch = make(chan byte)
var counter = 0
func f() {
counter++
<-ch // Block this goroutine
}
func main() {
flag.Parse()
if *n <= 0 {
fmt.Fprintf(os.Stderr, "invalid number of goroutines")
os.Exit(1)
}
// Limit the number of spare OS threads to just 1
runtime.GOMAXPROCS(1)
// Make a copy of MemStats
var m0 runtime.MemStats
runtime.ReadMemStats(&m0)
t0 := time.Now().UnixNano()
for i := 0; i < *n; i++ {
go f()
}
runtime.Gosched()
t1 := time.Now().UnixNano()
runtime.GC()
// Make a copy of MemStats
var m1 runtime.MemStats
runtime.ReadMemStats(&m1)
if counter != *n {
fmt.Fprintf(os.Stderr, "failed to begin execution of all goroutines")
os.Exit(1)
}
fmt.Printf("Number of goroutines: %d\n", *n)
fmt.Printf("Per goroutine:\n")
fmt.Printf(" Memory: %.2f bytes\n", float64(m1.Sys-m0.Sys)/float64(*n))
fmt.Printf(" Time: %f µs\n", float64(t1-t0)/float64(*n)/1e3)
}
一个简单的估算是假设每个goroutine分配了一个4 KiB的页面用于堆栈(4 KiB是一个相当统一的大小),再加上一些小的开销用于运行时的控制块(例如线程控制块);这与您观察到的情况相符(在2011年,Go 1.0之前)。因此,100,000个goroutines将占用约400 MiB的内存,而1,000,000个goroutines将占用约4 GiB的内存,在桌面上仍然可以管理,对于手机来说有点多,但在服务器上非常可管理。实际上,起始堆栈的大小范围从半页(2 KiB)到两页(8 KiB),因此这个估算是大致正确的。
起始堆栈大小随时间而变化;它最初为4 KiB(一个页面),然后在1.2中增加到8 KiB(2个页面),然后在1.4中减少到2 KiB(半个页面)。这些变化是由于分段堆栈在快速切换段("热堆栈分裂")时导致性能问题,因此在1.2中进行了增加以缓解问题,然后在将分段堆栈替换为连续堆栈时(1.4)进行了减少。Go 1.2 发行说明:堆栈大小:
在 Go 1.2 中,当创建 goroutine 时,堆栈的最小大小从 4KB 提高到了 8KB
Go 1.4 发行说明:运行时变更:
每个goroutine的内存主要是栈,它从低处开始增长,因此您可以廉价地拥有许多goroutine。您可以使用更小的初始栈,但这样它就必须更早地增长(以时间为代价获得空间),并且由于控制块不会缩小,收益会减少。在交换出时,可以消除堆栈(例如,在堆上执行所有分配,或在上下文切换时将堆栈保存到堆中),尽管这会降低性能并增加复杂性。这是可能的(如Erlang中所示),这意味着您只需要控制块和保存的上下文,允许goroutine数量的另一个因素为5×-10×,现在受控制块大小和goroutine本地变量的堆大小限制。然而,除非您需要数百万个微小的休眠goroutine,否则这并不是非常有用的。在 1.4 中,goroutine 的默认起始堆栈大小已从 8192 字节减少到 2048 字节。
由于拥有许多goroutine的主要用途是处理IO绑定任务(具体来说是处理阻塞系统调用,特别是网络或文件系统IO),因此您更有可能遇到其他资源的操作系统限制,即网络套接字或文件句柄:golang-nuts› goroutines和文件描述符的最大数量?。解决这个问题的常规方法是使用稀缺资源的池,或者更简单地通过信号量限制数量;请参见在Go中保留文件描述符和限制Go中的并发性。
这完全取决于您所运行的系统。但 goroutine 非常轻量级。一个平均进程应该没有问题处理 100,000 个并发的例程。当然,是否适用于您的目标平台,我们无法回答,除非知道该平台是什么。
这是一篇由Dave Cheney撰写的关于此主题的优秀文章:http://dave.cheney.net/2013/06/02/why-is-a-goroutines-stack-infinite
debug.SetMaxStack
来覆盖每个goroutine的新默认最大堆栈大小,分别为1GB和250MB(在64位和32位系统上)。也就是说,自Go1.2以来,goroutine堆栈大小并不是无限的。 - Dave C设置运行goroutine的数量阈值
在启动或停止goroutine时可以增加或减少计数器。
它可以等待最小或最大数量的goroutine运行,从而允许设置同时运行的gorc
受控goroutine的数量阈值。
n
进行比较之前都已经启动。你每次都这么幸运吗? :) - Filip Haglund2758.41字节
,运行的是Go 1.5.1版本。 - Filip Haglund