结构中的线程,函数参数对于新的 goroutine 来说太大

Thread within struct, function arguments too large for new goroutine

我创建了这个简单的应用程序来演示我遇到的问题。

package main

import (
    "fmt"
    "unsafe"
    "sync"
)

type loc_t struct {
    count       [9999]int64
    Counter     int64
}

func (l loc_t) rampUp (wg *sync.WaitGroup) {
    defer wg.Done()
    l.Counter += 1
}

func main() {
    wg := new(sync.WaitGroup)
    loc := loc_t{}

    fmt.Println(unsafe.Sizeof(loc))
    wg.Add(1)
    go loc.rampUp(wg)
    wg.Wait()
    fmt.Println(loc.Counter)
}

如果我 运行 以上我会得到 fatal error: newproc: function arguments too large for new goroutine runtime stack: runtime: unexpected return pc for runtime.systemstack called from 0x0

现在的原因是当 go 用于生成后台任务时的 2k 堆栈大小。有趣的是我只传递一个指针给被调用的函数。这个问题在生产中发生在我身上,显然不同的结构,一切都工作了一年,然后突然开始抛出这个错误。

堆栈大小的问题显然是结构本身的大小。因此,随着您的结构有机增长,您可以像我一样跨越 2k 堆栈调用大小。

可以通过在函数声明中使用指向结构的指针来解决上述问题。

func (l *loc_t) rampUp (wg *sync.WaitGroup) {
    defer wg.Done()
    l.Counter += 1
}

这会创建一个指向结构的指针,因此进入堆栈的只是指针,而不是结构的整个副本。

显然,如果您同时在多个线程中进行调用,这可能会产生其他影响,包括竞争条件。但作为一个不断增长的结构的解决方案,它会突然开始导致堆栈溢出,这是一个解决方案。

无论如何,希望这对其他人有帮助。

方法接收器被传递给方法调用,就像任何其他参数一样。因此,如果该方法具有非指针接收器,则将复制您案例中的整个结构。如果可以的话,最简单的解决方案是使用指针接收器。

如果您必须使用非指针接收器,那么您可以通过不将方法调用作为 goroutine 而是另一个函数(可能是函数文字)来启动方法调用来规避此问题:

go func() {
    loc.rampUp(wg)
}()

如果 loc 变量可能被同时修改(在启动的 goroutine 被调度并为 rampUp() 方法复制它之前),您可以手动创建它的副本并在goroutine,像这样:

loc2 := loc
wg.Add(1)
go func() {
    loc2.rampUp(wg)
}()

这些解决方案之所以有效,是因为启动新的 goroutine 不需要大的初始堆栈,因此初始堆栈限制不会妨碍。堆栈大小是动态的,因此在启动后它会根据需要增长。详情可以在这里阅读: