首页 > 解决方案 > 控制 OS 线程与 goroutine 的启动?

问题描述

Go 的一大优点是它拥有大量可用的轻量级线程(goroutines)。这开辟了一些有趣的战略机会,但在某些情况下,Go 往往会产生 OS 线程;默认情况下,这些通常限制为最多 10,000 个。如果你碰巧超过了这个数字,事情就会崩溃。

以下代码将我们的最大 OS 线程锁定为 5,然后尝试启动 10 个并发 goroutine。这些 goroutine 将尝试使用系统调用(在这种情况下,文件创建 / 写入 / 关闭),这往往意味着我们的 goroutine 将临时(并​​且专门)与 os 线程关联。一旦系统调用完成,我们的 goroutine 将再次成为一个不错的轻量级线程——很可能与其他 goroutine 共享一个 os 线程。

当然有一个简单的解决方案——不要那样写!事实上,如果你取出文件打开/写入/关闭,一切都会完美运行......但暂时假设,下面的代码结构代表了更大更卑鄙的东西,它不能真正改变很多:

package main

import (
    "fmt"
    "log"
    "os"
    "runtime/debug"
    "strconv"
    "sync"
)

func main() {
    MaxThreads := 5
    debug.SetMaxThreads(MaxThreads)

    var fileChan []chan string

    // Create 10 channels that we can use to send data to our file writers.
    for i := 0; i < 10; i++ {
        fileChan = append(fileChan, make(chan string))
    }

    var wg sync.WaitGroup

    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(n int, fChan chan string) {
            threadWriter(n, fChan)
            wg.Done()
        }(i, fileChan[i])
    }

    for i := 0; i < 10; i++ {
        for j := 0; j < 10; j++ {
            fileChan[i] <- "Test write " + strconv.Itoa(j) + " to chan " + strconv.Itoa(i)
        }
    }

    for i := 0; i < 10; i++ {
        close(fileChan[i])
    }

    wg.Wait()
    fmt.Println("All done - success")
}

func threadWriter(i int, dataChan chan string) {
    // Open the file
    f, err := os.Create("tmp/Thread-" + strconv.Itoa(i) + ".txt")
    if err != nil {
        log.Fatal("Cannot open thread file", i)
        return
    }
    // Wait for data to come in.
    for str := range dataChan {
        f.WriteString(str + "\n")
    }

    f.Close()
}

那么问题来了:(threadtest.go) 我将运行程序 20 次。有时它会正常工作,有时它会在运行时崩溃:程序超过 5 线程限制致命错误:线程耗尽

$ go build    
$ if [ ! -d tmp ]; then mkdir tmp; fi; for i in `seq 1 20`; do ./threadtest 2>/dev/null | grep success > /dev/null; if [ $? -eq 0 ]; then echo "all good"; else echo "NOPE - Threads exhausted"; fi; rm tmp/Thread-*.txt; done
NOPE - Threads exhausted
all good
NOPE - Threads exhausted
all good
all good
all good
NOPE - Threads exhausted
NOPE - Threads exhausted
NOPE - Threads exhausted
all good
NOPE - Threads exhausted
all good
all good
all good
all good
all good
all good
all good
all good
all good

有时有效,有时无效——这仅取决于系统调用的时间,以及当时处于“操作系统线程”状态的 goroutine 的数量。这很酷..操作系统线程必须有限制。

然而,与其恐慌,如果有某种方法可以检测我们是否即将超过极限并旋转,那可能会正常......即:

func threadWriter(i int, dataChan chan string) {
    while(debug.OsThreadsActive() > 9999) {
      // spin here until we are safe
    }

    // Open the file
    f, err := os.Create("tmp/Thread-" + strconv.Itoa(i) + ".txt")
    if err != nil {
        log.Fatal("Cannot open thread file", i)
        return
    }
    // Wait for data to come in.
    for str := range dataChan {
        f.WriteString(str + "\n")
    }

    f.Close()
}

不幸的是,查看 runtime/proc.go,似乎没有一个简单的接口可以从中获取数据。mcount() 不是我可以查询的(实际上也不安全)。

那么 - 有没有人有任何建议,给定超过 MaxThreads 的 goroutine 计数,以及在这些 goroutines 中使用潜在阻塞 os 调用的要求,以尝试停止访问 MaxThreads?或者,我是否只需要在我的 goroutine 中的任何潜在系统调用周围放置一个数量有限的互斥锁,然后尝试代表 Go 限制并发线程?

标签: multithreadinggogoroutine

解决方案


推荐阅读