我可以无痛地使用多少个goroutine?例如,维基百科说,在Erlang中,可以创建2000万个进程,而不会降低性能。
更新:我刚刚对goroutines性能进行了一些调查,并得到了这样的结果:
- 看起来goroutine的生存期比计算sqrt()的次数要多1000倍(对我而言约为45µs),唯一的限制是内存
- Goroutine的成本4 — 4.5 KB
我可以无痛地使用多少个goroutine?例如,维基百科说,在Erlang中,可以创建2000万个进程,而不会降低性能。
更新:我刚刚对goroutines性能进行了一些调查,并得到了这样的结果:
Answers:
如果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
在装有4 GB内存的计算机上,这会将goroutine的最大数量限制为略少于100万。
源代码(如果您已经理解上面打印的数字,则无需阅读此书):
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)
}
n
。每次都幸运吗?:)
2758.41 bytes
每个goroutine的go操场报告,运行go 1.5.1。
每个Go数十万个常见问题解答:为什么使用goroutines而不是线程?:
在同一地址空间中创建数十万个goroutine是很实际的。
测试test / chan / goroutines.go可以创建10,000,并且可以轻松完成更多任务,但是旨在快速运行。您可以更改系统上的数字以进行实验。如果有足够的内存,您可以轻松地运行数百万,例如在服务器上。
要了解goroutine的最大数量,请注意,每goroutine的成本主要是堆栈。再次按常见问题解答:
…goroutines可能非常便宜:除了栈的内存(只有几千字节)之外,它们的开销很小。
后端计算是假设每个goroutine为堆栈分配了一个4 KiB页面(4 KiB是相当均匀的大小),再加上一个控制块(如Thread Control Block)的少量开销运行时;这与您观察到的结果一致(2011年,Go 1.0之前的版本)。因此,100 Ki例程将占用约400 MiB的内存,而1 Mi例程将占用约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堆栈的默认起始大小(从1.4开始)已从8192字节减少到2048字节。
每个goroutine的内存主要是堆栈,它开始时很低并且会增长,因此您可以廉价地拥有许多goroutine。您可以使用较小的起始堆栈,但随后必须更快地增长(以时间为代价获得空间),并且由于控制块未收缩而使收益降低。至少在换出时可以消除堆栈(例如,在堆上进行所有分配,或在上下文切换时将堆栈保存到堆中),尽管这会影响性能并增加复杂性。这是可能的(就像在Erlang中一样),这意味着您只需要控制块和保存的上下文,就可以使goroutine的数量增加5倍至10倍,现在受goroutine的控制块大小和堆上大小的限制-局部变量。但是,这并不是非常有用的,除非您需要数百万个微小的睡眠goroutine。
由于拥有许多goroutine的主要用途是用于IO绑定任务(具体用于处理阻塞的系统调用,特别是网络或文件系统IO),因此您更有可能遇到其他资源(即网络套接字或文件句柄)对OS的限制:golang-nuts› goroutine和文件描述符的最大数量?。解决此问题的通常方法是使用稀缺资源池,或更简单地说,通过信号量限制数量;请参见Go中的保存文件描述符和Go中的限制并发。
这完全取决于您所运行的系统。但是goroutine非常轻巧。一个平均进程应该不会有100.000个并发例程的问题。当然,这是否适用于您的目标平台,是我们在不知道该平台是什么的情况下无法回答的。
换句话说,有谎言,该死的谎言和基准。正如Erlang基准测试的作者所承认的那样,
不用说,机器中没有足够的内存来实际执行任何有用的操作。压力测试erlang
您的硬件是什么,操作系统是什么,基准源代码在哪里?试图衡量和证明/否定的基准是什么?
这是戴夫·切尼(Dave Cheney)撰写的有关该主题的精彩文章:http : //dave.cheney.net/2013/06/02/why-is-a-goroutines-stack-infinite
debug.SetMaxStack
覆盖了1 GB和250 MB的“新的”默认最大按goroutine堆栈大小(分别在64位和32位系统上)。即goroutine堆栈的大小自Go1.2起就没有限制。
如果goroutine的数量成为问题,则可以轻松地将其限制在您的程序中:
请参见mr51m0n / gorc和本示例。
设置正在运行的goroutine的阈值
在启动或停止goroutine时可以增加和减少计数器。
它可以等待最小或最大数量的goroutine运行,从而允许为gorc
同时运行的受控goroutine数量设置阈值。