为什么在golang中多个goroutine被阻塞写文件时,并没有创建很多线程?

12

我们知道,在Go语言中,当协程需要执行阻塞调用(例如系统调用或通过cgo调用C库)时,可能会创建一个线程。以下是一些测试代码:

   package main

   import (
        "io/ioutil"
        "os"
        "runtime"
        "strconv"
    )

    func main() {
        runtime.GOMAXPROCS(2)
        data, err := ioutil.ReadFile("./55555.log")
        if err != nil {
            println(err)
            return
        }
        for i := 0; i < 200; i++ {
            go func(n int) {
                for {
                    err := ioutil.WriteFile("testxxx"+strconv.Itoa(n), []byte(data), os.ModePerm)
                    if err != nil {
                        println(err)
                        break
                    }
                }
            }(i)
        }
        select {}
    }

当我运行它时,它没有创建很多线程。
➜ =99=[root /root]$ cat /proc/9616/status | grep -i thread
Threads:    5

有什么想法吗?
3个回答

13

我稍微修改了你的程序,以输出一个更大的代码块。

package main

import (
    "io/ioutil"
    "os"
    "runtime"
    "strconv"
)

func main() {
    runtime.GOMAXPROCS(2)
    data := make([]byte, 128*1024*1024)
    for i := 0; i < 200; i++ {
        go func(n int) {
            for {
                err := ioutil.WriteFile("testxxx"+strconv.Itoa(n), []byte(data), os.ModePerm)
                if err != nil {
                    println(err)
                    break
                }
            }
        }(i)
    }
    select {}
}

这样就像您预期的那样显示了超过200个线程

$ cat /proc/17033/status | grep -i thread
Threads:    203

所以我认为在你最初的测试中,系统调用退出得太快,无法显示你预期的效果。


谢谢,这对我很有帮助。我犯了一个错误,在我的情况下使用了 go run 运行程序。 - frank.lin
@frank.lin,你的意思是构建和运行二进制文件有区别吗? - ipopa
所以我认为在您的原始测试中,系统调用退出得太快,无法显示您期望的效果。这是否意味着即使对于IO操作,仍然有机会P不会从当前的M分离?还是其他一些原因导致不会产生新线程? - atline
@atline 是的,你说得对。这是一篇非常好的文章,帮助了我很多。 https://www.ardanlabs.com/blog/2018/08/scheduling-in-go-part2.html - Shubhang b
还有为什么在goroutine内部有一个无限循环?即使没有它,代码也应该产生相同的输出。 - Shubhang b

6

一个goroutine是一个轻量级的线程,它不等同于操作系统线程。语言规范将其定义为“在相同地址空间内独立并发控制的线程”。

引用runtime包文档:

GOMAXPROCS变量限制了可以同时执行用户级Go代码的操作系统线程数。可以阻塞在系统调用中代表Go代码的线程数没有限制;它们不计入GOMAXPROCS限制。

仅仅因为你启动了200个goroutines,并不意味着会为它们启动200个线程。你将GOMAXPROCS设置为2,这意味着同时可以运行2个“活跃”的goroutine。如果goroutine被阻塞(例如I/O等待),则可能会产生新的线程。你没有提及测试文件的大小,启动的goroutine可能会很快完成写入。

Effective Go 博客文章将它们定义为:

它们被称为 goroutines,因为现有的术语——线程、协程、进程等——传达了不准确的内涵。 goroutine 具有简单的模型:它是在同一地址空间中与其他 goroutine 并发执行的函数。 它很轻量级,成本仅仅比分配堆栈空间多一点。 而且堆栈从开始就很小,所以它们很便宜,并通过分配(和释放)堆存储器来增长。

如果一个 goroutine 阻塞(例如等待 I/O),则它们会被复用到多个 OS 线程上,以便其他 goroutine 继续运行。 它们的设计隐藏了许多线程创建和管理的复杂性。


3
And since you explicitly set GOMAXPROCS to 2, you can't expect it to spawn 200 threads.”这个说法是不正确的。每个阻塞系统调用都可以创建一个新的线程。 - Nick Craig-Wood
@NickCraig-Wood,您能否看一下这个问题 - atline

2
本文讨论如何限制实际创建的线程数(而非 goroutine)。Go 1.2 引入了线程限制管理,详情请见提交记录 665feee。你可以在 pkg/runtime/crash_test.go#L128-L134 中看到一个测试以检查是否已达到实际创建的线程数。issue 4056 commit 665feee pkg/runtime/crash_test.go#L128-L134
func TestThreadExhaustion(t *testing.T) {
    output := executeTest(t, threadExhaustionSource, nil)
    want := "runtime: program exceeds 10-thread limit\nfatal error: thread exhaustion"
    if !strings.HasPrefix(output, want) {
        t.Fatalf("output does not start with %q:\n%s", want, output)
    }
}

同样的文件中有一个示例,可以创建一个实际的线程(针对给定的 goroutine),使用 runtime.LockOSThread()
func testInNewThread(name string) {
    c := make(chan bool)
    go func() {
        runtime.LockOSThread()
        test(name)
        c <- true
    }()
    <-c
}

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