Go程序中主协程和衍生协程的区别

4
创建使用gRPC的服务器时,如果我在主进程中启动gRPC服务器,它可以处理来自客户端的多达数千个请求。但是,如果我将服务器作为goroutine启动,则只能处理一些请求(数百个),然后陷入停顿。我已经使用非常简单的示例进行了测试和确认,google.golang.org/grpc/examples/helloworld。

这是因为生成的goroutine堆栈大小非常小(2Kbytes),而主goroutine的堆栈要大得多吗?主goroutine和生成的goroutine之间有什么区别?
例如link。将示例的修改部分如下。

greeter_server/main.go

func main() {
    go func() {
        lis, err := net.Listen("tcp", port)
        if err != nil {
            log.Fatalf("failed to listen: %v", err)
        }   
        s := grpc.NewServer()
        pb.RegisterGreeterServer(s, &server{})
        s.Serve(lis)
    }() 

    for {
    }   
}

greeter_client/main.go

func main() {
    // Set up a connection to the server.
    for i := 0; i < 500; i++ {
        conn, err := grpc.Dial(address, grpc.WithInsecure())
        if err != nil {
            log.Fatalf("did not connect: %v", err)
        }
        defer conn.Close()
        c := pb.NewGreeterClient(conn)

        for i := 0; i < 500; i++ {
            // Contact the server and print out its response.
            name := defaultName
            if len(os.Args) > 1 {
                name = os.Args[1]
            }
            r, err := c.SayHello(context.Background(), &pb.HelloRequest{Name: name})
            if err != nil {
                log.Fatalf("could not greet: %v", err)
            }
            log.Printf("%d's Greeting: %s", i, r.Message)
        }
    }
}

它们是相同的。你能展示一下你正在做的具体例子吗? - JimB
@JimB 感谢您的回复。我已经包含了示例链接和修改后的代码。 - v1ct0r
@Amd 这是 go1.7 linux/amd64。 - v1ct0r
2
你的主函数中有一个繁忙循环,请避免这样做。 - JimB
@JimBI加了一些延时代码,现在不会再卡住了。你能告诉我原因吗? - v1ct0r
3
忙循环总是一个错误。没有理由让 CPU 以 100% 的占用率运转只是为了阻塞。根本不要使用 for 循环,等待某个能够阻塞的东西即可。在这个例子中,没有理由使用 goroutine,只需要内联调用那段代码即可。如果你真的需要阻塞但没有任何操作可以进行,可以使用一个空的 select{},但通常情况下并不需要。 - JimB
1个回答

7
为什么 Goroutine 的栈是无限的:
Goroutine 的一个关键特点是它们的成本;它们在初始内存占用方面很便宜(与传统的 POSIX 线程需要 1 到 8MB 相比),它们的堆栈会根据需要增长和缩小。这使得 Goroutine 可以从单个 4096 字节的堆栈开始,随着需要而增长和缩小,而不会有耗尽堆栈的风险。
然而,直到现在我才保留了一个细节,它将意外使用递归函数与操作系统的严重内存耗尽联系起来,即当需要新的堆栈页时,它们将从堆中分配。
随着您的无限函数继续调用自身,新的堆栈页面将从堆中分配,允许该函数一遍又一遍地调用自身。相当快地,堆的大小将超过机器上可用的空闲物理内存量,在此时,交换将很快使您的机器无法使用。
Go 程序可用的堆大小取决于许多因素,包括 CPU 架构和操作系统,但通常表示的是超过机器物理内存的内存量,因此您的程序在耗尽其堆之前可能会导致机器大量交换。
参考:http://dave.cheney.net/2013/06/02/why-is-a-goroutines-stack-infinite 空循环:
for{
}

使用100%的CPU核心等待某些操作,根据用例,您可以使用以下内容:
- sync.WaitGroup,例如此处
- select {},例如此处
- 通道
- time.Sleep


是因为生成的goroutine堆栈大小非常小(2K字节),而主goroutine的堆栈要大得多吗?

不是的,您可以尝试这两个示例,以查看goroutine的堆栈限制是否相同:
一个主goroutine在Go Playground上,
Go Playground上尝试第二个goroutine:

package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup

func main() {
    wg.Add(1)
    go run()
    wg.Wait()
}
func run() {
    s := &S{a: 1, b: 2}
    fmt.Println(s)
    wg.Done()
}

type S struct {
    a, b int
}

// String implements the fmt.Stringer interface
func (s *S) String() string {
    return fmt.Sprintf("%s", s) // Sprintf will call s.String()
}

在 Go Playground 上,这两个输出结果是相同的:

runtime: goroutine stack exceeds 250_000_000-byte limit
fatal error: stack overflow

在具有 8 GB RAM 的个人电脑上的输出:

runtime: goroutine stack exceeds 1_000_000_000-byte limit
fatal error: stack overflow

1
感谢您的详细解释! - v1ct0r

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