Multithreading 控制操作系统线程与goroutine的启动?
Go最棒的一点是,它拥有大量可用的轻量级线程(GoRoutine)。这开启了一些有趣的战略机遇,但在某些情况下,Go倾向于产生操作系统线程;默认情况下,这些值通常限制在10000以内。如果你碰巧超过这个数字,事情就会崩溃 下面的代码将我们的max OS线程锁定为5个,然后尝试启动10个并发goroutine。这些goroutine将尝试使用系统调用(在本例中为file create/write/close),这往往意味着我们的goroutine将暂时(并且独占地)与os线程关联。一旦系统调用完成,我们的goroutine将再次成为一个好的轻量级线程——很可能与其他goroutine共享一个os线程 当然,有一个简单的解决方案——不要这样写!事实上,如果你打开/写入/关闭文件,一切都会很好地工作。。。但假设目前,下面的代码结构代表的是一些更大、更卑鄙的东西,它们不会真正改变很多:Multithreading 控制操作系统线程与goroutine的启动?,multithreading,go,goroutine,Multithreading,Go,Goroutine,Go最棒的一点是,它拥有大量可用的轻量级线程(GoRoutine)。这开启了一些有趣的战略机遇,但在某些情况下,Go倾向于产生操作系统线程;默认情况下,这些值通常限制在10000以内。如果你碰巧超过这个数字,事情就会崩溃 下面的代码将我们的max OS线程锁定为5个,然后尝试启动10个并发goroutine。这些goroutine将尝试使用系统调用(在本例中为file create/write/close),这往往意味着我们的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()
}
有时有效,有时无效-这取决于系统调用的时间,以及当时有多少goroutine处于“os线程”状态。这很酷。。操作系统线程必须有一个限制
然而,如果有某种方法可以检测到我们是否即将超过极限,并旋转,而不是惊慌失措,那么这可能会解决问题。。即:
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()不是我可以查询的东西(现实情况下,这样做也不安全)
那么,有没有人建议,如果goroutine的数量超过了MaxThreads,并且要求在这些goroutine中使用潜在的阻塞操作系统调用,如何尝试并停止攻击MaxThreads?或者,我是否只需要在goroutine中的任何潜在系统调用周围设置一个数量有限的互斥锁,从而尝试代表Go限制并发线程?理想情况下,运行库应该为您无形地完成这项工作。不幸的是,很难做到正确。现有的系统确实有很多魔法可以避免在可能的情况下使用阻塞系统调用,例如,大多数文件I/O实际上都是通过
epoll
或kqueues或系统提供的任何东西的包装来完成的。谢谢@torek-是的,当我用C编写了模糊类似的东西时,epoll()避免触发额外线程非常棒。即使完全序列化操作系统调用也不能完全工作。在互斥体中包装open/write/close仍然失败(大约1000次中有2次)。我猜,线程捕获可能由垃圾回收器完成-因此,系统调用完成后,它不会立即返回到标准goroutine-因此,甚至可以将系统调用互斥以序列化,实际上并不完全有效。我会避免修改线程数,除非你有很好的理由这么做。情况似乎并非如此——没有问题语句会提示线程数的更改。理想情况下,运行时应该在不可见的情况下为您执行此操作。不幸的是,很难做到正确。现有的系统确实有很多魔法可以避免在可能的情况下使用阻塞系统调用,例如,大多数文件I/O实际上都是通过epoll
或kqueues或系统提供的任何东西的包装来完成的。谢谢@torek-是的,当我用C编写了模糊类似的东西时,epoll()避免触发额外线程非常棒。即使完全序列化操作系统调用也不能完全工作。在互斥体中包装open/write/close仍然失败(大约1000次中有2次)。我猜,线程捕获可能由垃圾回收器完成-因此,系统调用完成后,它不会立即返回到标准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()
}