我可以无痛使用多少个 goroutine?例如,维基百科说,在 Erlang 中可以创建 2000 万个进程而不会降低性能。
更新:我刚刚对 goroutines 的性能进行了一些调查,得到了这样的结果:
- 看起来 goroutine 的生命周期超过了计算 sqrt() 1000 次(对我来说约为 45µs),唯一的限制是内存
- Goroutine 花费 4 — 4.5 KB
我可以无痛使用多少个 goroutine?例如,维基百科说,在 Erlang 中可以创建 2000 万个进程而不会降低性能。
更新:我刚刚对 goroutines 的性能进行了一些调查,得到了这样的结果:
如果一个 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)
}
数十万,每个 Go 常见问题解答:为什么使用 goroutines 而不是线程?:
在同一个地址空间中创建数十万个 goroutine 是很实用的。
测试test/chan/goroutines.go创建了 10,000 个并且可以轻松完成更多任务,但旨在快速运行;您可以更改系统上的数字以进行实验。如果有足够的内存,您可以轻松地运行数百万,例如在服务器上。
要了解 goroutine 的最大数量,请注意每个 goroutine 的成本主要是堆栈。再次根据常见问题解答:
…goroutines,可能非常便宜:除了堆栈内存之外,它们几乎没有开销,只有几千字节。
粗略的计算是假设每个 goroutine 有一个 4 KiB的页面分配给堆栈(4 KiB 是一个相当统一的大小),加上一些小的开销用于控制块(如线程控制块)用于运行时;这与您观察到的一致(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 发行说明:对运行时的更改:
1.4 中 goroutine 堆栈的默认起始大小已从 8192 字节减少到 2048 字节。
每个 goroutine 的内存主要是堆栈,它从低位开始并不断增长,因此您可以廉价地拥有许多 goroutine。您可以使用较小的起始堆栈,但它必须更快地增长(以时间为代价获得空间),并且由于控制块没有缩小,收益会减少。可以消除堆栈,至少在换出时(例如,在堆上进行所有分配,或在上下文切换时将堆栈保存到堆),但这会损害性能并增加复杂性。这是可能的(就像在 Erlang 中一样),并且意味着您只需要控制块和保存的上下文,允许 goroutine 数量的另一个因子 5×–10×,现在受控制块大小和 goroutine 堆大小的限制-局部变量。然而,这并不是非常有用,除非您需要数百万个微小的休眠 goroutine。
由于拥有许多 goroutine 的主要用途是用于 IO 绑定任务(具体来说是处理阻塞系统调用,特别是网络或文件系统 IO),因此您更有可能遇到操作系统对其他资源的限制,即网络套接字或文件句柄: golang-nuts › goroutines 和文件描述符的最大数量?. 解决这个问题的常用方法是使用稀有资源池,或者更简单地通过信号量限制数量;请参阅在 Go 中保存文件描述符和在 Go 中限制并发。
这完全取决于您运行的系统。但是 goroutine 非常轻量级。一个平均进程应该没有 100.000 个并发例程的问题。当然,这是否适用于您的目标平台是我们在不知道该平台是什么的情况下无法回答的问题。
换句话说,有谎言,该死的谎言和基准。正如 Erlang 基准测试的作者所承认的,
不用说,机器中没有足够的内存来实际做任何有用的事情。压力测试erlang
你的硬件是什么,你的操作系统是什么,你的基准源代码在哪里?试图衡量和证明/反驳的基准是什么?
这是 Dave Cheney 关于这个主题的一篇很棒的文章:http: //dave.cheney.net/2013/06/02/why-is-a-goroutines-stack-infinite
如果 goroutine 的数量成为问题,您可以轻松地为您的程序限制它:
参见mr51m0n/gorc和这个示例。
设置正在运行的 goroutine 数量的阈值
可以在启动或停止 goroutine 时增加和减少计数器。
它可以等待最小或最大数量的 goroutine 运行,从而允许为gorc
同时运行的受管 goroutine 的数量设置阈值。