最大goroutine数量

103

我可以毫不费力地使用多少个goroutine呢? 例如,维基百科说,在Erlang中可以创建2000万个进程而不降低性能。

更新: 我刚刚研究了一下goroutine的性能,得出了以下结果:

  • 似乎goroutine的生命周期长于计算sqrt() 1000次(对我来说大约是45µs),唯一的限制是内存
  • Goroutine的成本为4-4.5 KB
7个回答

95

如果一个 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)
}

3
你从每个goroutine的大约4k(这在不同版本中有所变化;你还需要考虑goroutine堆栈的使用情况)转换为基于安装内存的最大值是错误的。最大值应该基于可寻址虚拟内存(通常为32位操作系统的2-3GB),或物理内存加上可用的交换空间,或进程的内存资源限制(通常为无限制)中较小的一个。例如,在具有合理交换设置的64位计算机上,安装的物理内存对任何限制都不重要(但当开始进行交换时,性能将会降低)。 - Dave C
我认为这里存在竞态条件,因为没有明确的同步来确保所有goroutine在计数器与n进行比较之前都已经启动。你每次都这么幸运吗? :) - Filip Haglund
3
Go Playground报告每个goroutine使用 2758.41字节,运行的是Go 1.5.1版本。 - Filip Haglund
1
正如@FilipHaglund所指出的,数字随着时间的推移而改变;这主要是由于起始堆栈大小的变化(4 KiB,然后在1.2中为8 KiB,然后在1.4中为2 KiB)。 - Nils von Barth
在i7 9750H上运行Go 1.13(每个goroutine): 内存:9068.51字节, 时间:3.460311微秒,仍然相当合理,无法想象任何情况下goroutine本身会成为瓶颈,而不是它执行的逻辑。 - Marco
显示剩余3条评论

32
根据Go FAQ(为什么使用goroutine而不是线程?),可以在同一地址空间中创建数十万个goroutine。
测试test/chan/goroutines.go创建了1万个goroutine,但可以轻松地创建更多,只要系统内存足够,例如在服务器上可以轻松运行数百万个。
要了解goroutine的最大数量,请注意每个goroutine的成本主要是堆栈。根据FAQ:
“… goroutine可能非常便宜:除了堆栈内存外,它们几乎没有开销,堆栈内存只有几KB。”

一个简单的估算是假设每个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 发行说明:运行时变更

在 1.4 中,goroutine 的默认起始堆栈大小已从 8192 字节减少到 2048 字节。

每个goroutine的内存主要是栈,它从低处开始增长,因此您可以廉价地拥有许多goroutine。您可以使用更小的初始栈,但这样它就必须更早地增长(以时间为代价获得空间),并且由于控制块不会缩小,收益会减少。在交换出时,可以消除堆栈(例如,在堆上执行所有分配,或在上下文切换时将堆栈保存到堆中),尽管这会降低性能并增加复杂性。这是可能的(如Erlang中所示),这意味着您只需要控制块和保存的上下文,允许goroutine数量的另一个因素为5×-10×,现在受控制块大小和goroutine本地变量的堆大小限制。然而,除非您需要数百万个微小的休眠goroutine,否则这并不是非常有用的。

由于拥有许多goroutine的主要用途是处理IO绑定任务(具体来说是处理阻塞系统调用,特别是网络或文件系统IO),因此您更有可能遇到其他资源的操作系统限制,即网络套接字或文件句柄:golang-nuts› goroutines和文件描述符的最大数量?。解决这个问题的常规方法是使用稀缺资源的,或者更简单地通过信号量限制数量;请参见在Go中保留文件描述符限制Go中的并发性


1
在Go中限制并发是一个非常好的和简单的例子。 - gabuzo

8

这完全取决于您所运行的系统。但 goroutine 非常轻量级。一个平均进程应该没有问题处理 100,000 个并发的例程。当然,是否适用于您的目标平台,我们无法回答,除非知道该平台是什么。


你在基于ARM的平板电脑上没有遇到任何问题吗? - peterSO
1
由于我没有基于ARM的平板电脑,所以我无法说。但是这个观点仍然成立。如果不知道目标系统的能力,就无法判断。 - jimt
2
换句话说,如果没有适当的上下文,你声称“100,000个并发例程没有问题”是毫无意义的。 - peterSO
5
你把它的上下文给忽略了。这句话的意思是“一个普通的进程应该在100,000个并发例程下没有问题”。 - jimt

8
为了概括一下,有谎言、该死的谎言和基准测试。正如Erlang基准测试作者所承认的那样,
“不用说,机器上剩余的内存不足以实际执行任何有用的操作。”压力测试erlang 你的硬件是什么,你的操作系统是什么,你的基准测试源代码在哪里?基准测试试图测量和证明/否定什么?

2

1
请注意,链接的文章有点过时了。自Go1.2以来,已经有debug.SetMaxStack 来覆盖每个goroutine的新默认最大堆栈大小,分别为1GB和250MB(在64位和32位系统上)。也就是说,自Go1.2以来,goroutine堆栈大小并不是无限的。 - Dave C

0
如果goroutine的数量成为问题,您可以轻松地为程序限制它:
请参见mr51m0n/gorcthis example

设置运行goroutine的数量阈值

在启动或停止goroutine时可以增加或减少计数器。
它可以等待最小或最大数量的goroutine运行,从而允许设置同时运行的gorc受控goroutine的数量阈值。


-2

当操作是CPU密集型时,超过核心数量的任何东西都被证明是无用的。

在任何其他情况下,您需要自行测试。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接