最大例行程序数


71

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

更新:我刚刚对goroutines性能进行了一些调查,并得到了这样的结果:

  • 看起来goroutine的生存期比计算sqrt()的次数要多1000倍(对我而言约为45µs),唯一的限制是内存
  • Goroutine的成本4 — 4.5 KB

Answers:


63

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

2
从〜4k /每个goroutine(每个版本的版本有所更改;并且还需要考虑goroutine堆栈的使用)到基于已安装内存的最大值的转换存在缺陷。最大值将基于可寻址虚拟内存(对于32位操作系统,通常为2-3GB),物理内存加上可用的交换空间或进程的内存资源限制(通常为无限制)中的较小者。例如,在具有合理交换设置的64位计算机上,安装的物理内存与任何限制无关(但是随着交换开始发生,性能会下降)。
Dave C

我认为这包含一个竞争条件,因为没有明确的同步来确保在将计数器与进行比较之前已启动所有goroutine n。每次都幸运吗?:)
Filip Haglund

2
2758.41 bytes每个goroutine的go操场报告,运行go 1.5.1。
菲利普·哈格隆德

1
正如@FilipHaglund指出的那样,数字已经随着时间而改变;这主要是由于开始堆栈大小更改(4 KiB,然后是1.2中的8 KiB,然后是1.4中的2 KiB)。
尼尔斯·冯·巴特

他进行了数学运算
公民conn

20

每个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中的限制并发


1
限制go中的并发是一个非常简单的好例子
gabuzo

6

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


您在基于ARM的平板电脑上没有问题吗?
peterSO 2011年

1
由于我没有基于ARM的平板电脑,所以我不能说。重点仍然存在。在不知道目标系统可以做什么的情况下无法分辨。
2011年

1
换句话说,如果没有适当的上下文,您的“ 100.000个并发例程没有问题”的主张毫无意义。
peterSO 2011年

4
您将其带出上下文。句子中写着“一个平均进程应该对100.000个并发例程没有问题”。
jimt 2011年

5

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

不用说,机器中没有足够的内存来实际执行任何有用的操作。压力测试erlang

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



0

如果goroutine的数量成为问题,则可以轻松地将其限制在您的程序中:
请参见mr51m0n / gorc本示例

设置正在运行的goroutine的阈值

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.