【问题标题】:Max number of goroutines最大 goroutine 数
【发布时间】:2012-01-20 12:04:43
【问题描述】:

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

更新:我刚刚investigated in goroutines performance一点,得到了这样的结果:

  • 看起来 goroutine 的生命周期比计算 sqrt() 1000 次要多(对我来说约为 45µs),唯一的限制是内存
  • Goroutine 花费 4 — 4.5 KB

【问题讨论】:

    标签: go multitasking goroutine


    【解决方案1】:

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

    【讨论】:

    • 您从 ~4k/per goroutine(这已从发布版本到发布版本;并且您还需要考虑 goroutine 堆栈使用情况)到基于已安装内存的最大值的转换是有缺陷的。最大值将基于可寻址虚拟内存(32 位操作系统通常为 2-3GB)、物理内存加上可用交换空间或进程的内存资源限制(通常无限制)中较小的一个)。例如。在具有健全交换设置的 64 位机器上,安装的物理内存与任何 限制 无关(但随着交换开始发生,性能会降低)。
    • 我认为这包含一个竞争条件,因为没有明确的同步来确保在计数器与n 进行比较之前所有 goroutines 都已启动。你每次都很幸运吗? :)
    • go 操场报告 2758.41 bytes 每个 goroutine,运行 go 1.5.1。
    • 正如@FilipHaglund 所说,数字随着时间而变化;这主要是由于开始堆栈大小发生变化(4 KiB,然后是 1.2 中的 8 KiB,然后是 1.4 中的 2 KiB)。
    • 他算了算
    【解决方案2】:

    数十万,每个 Go 常见问题解答:Why goroutines instead of threads?

    在同一个地址空间中创建数十万个 goroutine 是很实用的。

    测试test/chan/goroutines.go 可以创建 10,000 个并且可以轻松完成更多任务,但旨在快速运行;您可以更改系统上的数字以进行实验。如果有足够的内存,您可以轻松地运行数百万次,例如在服务器上。

    要了解 goroutine 的最大数量,请注意每个 goroutine 的成本主要是堆栈。再次按照常见问题解答:

    …goroutines,可以非常便宜:除了堆栈内存之外,它们的开销很小,只有几千字节。

    粗略计算是假设每个 goroutine 有一个 4 KiB page 分配给堆栈(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 发行说明:Stack size

    在 Go 1.2 中,创建 goroutine 时堆栈的最小大小已从 4KB 提升到 8KB

    Go 1.4 发行说明:Changes to the runtime

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

    每个 goroutine 的内存主要是堆栈,它从低位开始并不断增长,因此您可以廉价地拥有许多 goroutine。您可以使用较小的起始堆栈,但它必须更快地增长(以时间为代价获得空间),并且由于控制块没有缩小,收益会减少。可以消除堆栈,至少在换出时(例如,在堆上进行所有分配,或在上下文切换时将堆栈保存到堆),但这会损害性能并增加复杂性。这是可能的(就像在 Erlang 中一样),并且意味着您只需要控制块和保存的上下文,允许 goroutine 数量的另一个因子 5×–10×,现在受控制块大小和 goroutine 堆大小的限制-局部变量。但是,这并不是非常有用,除非您需要数百万个微小的休眠 goroutine。

    由于拥有许多 goroutine 的主要用途是执行 IO 绑定任务(具体来说是处理阻塞系统调用,特别是网络或文件系统 IO),因此您更有可能遇到操作系统对其他资源(即网络套接字)的限制或文件句柄:golang-nuts › The max number of goroutines and file descriptors?。解决这个问题的常用方法是使用稀有资源的pool,或者更简单地通过semaphore 限制数量;见Conserving File Descriptors in GoLimiting Concurrency in Go

    【讨论】:

    【解决方案3】:

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

    【讨论】:

    • 您在基于 ARM 的平板电脑上没有问题吗?
    • 由于我没有基于 ARM 的平板电脑,我不能说。不过,这一点仍然存在。如果不知道目标系统可以做什么,就无法判断。
    • 换句话说,如果没有适当的上下文,您声称“100.000 个并发例程没有问题”是毫无意义的。
    • 你断章取义了。这句话的意思是“一个平均进程应该没有 100.000 个并发例程的问题”。
    【解决方案4】:

    换句话说,有谎言、该死的谎言和基准。正如 Erlang 基准测试的作者所承认的,

    不用说,没有足够的内存留在 机器实际上做任何有用的事情。 stress-testing erlang

    您的硬件是什么,您的操作系统是什么,您的基准测试源代码在哪里?试图衡量和证明/反驳的基准是什么?

    【讨论】:

      【解决方案5】:

      这是 Dave Cheney 关于这个主题的一篇很棒的文章:http://dave.cheney.net/2013/06/02/why-is-a-goroutines-stack-infinite

      【讨论】:

      • 请注意链接的文章有点过时了。从 Go1.2 开始,debug.SetMaxStack 覆盖了“新的”默认最大每个 goroutine 堆栈大小 1 GB 和 250 MB(分别在 64 位和 32 位系统上)。 IE。自 Go1.2 以来,goroutine 堆栈大小不是是无限的。
      【解决方案6】:

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

      设置正在运行的 goroutine 数量的阈值

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

      【讨论】:

        【解决方案7】:

        当操作CPU受限时,任何超出内核数量的东西都被证明什么都不做。

        在任何其他情况您都需要自己测试。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-09-19
          • 2017-11-29
          • 2010-12-16
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多