极端情况下收缩 Go 的线程数
在 Go 的 runtime 里有一些创建了就没法回收的东西。
之前在 这篇 里讲过 allgs 没法回收的问题。
除了 allgs 之外,当前 Go 创建的线程也是没法退出的,比如这个来自 xiaorui.cc 的例子,我简单做了个修改,能从网页看到线程:
package main
/*
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void output(char *str) {
usleep(1000000);
printf("%s\n", str);
}
*/
import "C"
import "unsafe"
import "net/http"
import _ "net/http/pprof"
func init() {
go http.ListenAndServe(":9999", nil)
}
func main() {
for i := 0;i < 1000;i++ {
go func(){
str := "hello cgo"
//change to char*
cstr := C.CString(str)
C.output(cstr)
C.free(unsafe.Pointer(cstr))
}()
}
select{}
}
可见 Goroutine 退出了,历史上创建的线程也是不会退出的。之前我也一直认为没有办法退出这些线程,不过这周被同事教育,还是有办法的。
虽然问题直到现在依然没解决,但是这个 issue 里也提供了一种邪道解决办法,直接调用 LockOSThread,而不调用 Unlock,这样在退出的时候和当前 g 绑定的线程就会直接销毁:
把开头的程序改改,增加一个接口,killThread。
package main
/*
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void output(char *str) {
usleep(1000000);
printf("%s\n", str);
}
*/
import "C"
import (
"net/http"
"unsafe"
"log"
_ "net/http/pprof"
"runtime"
"sync"
)
func init() {
go http.ListenAndServe(":9999", nil)
}
func main() {
for i := 0; i < 1000; i++ {
go func() {
str := "hello cgo"
//change to char*
cstr := C.CString(str)
C.output(cstr)
C.free(unsafe.Pointer(cstr))
}()
}
killThreadService()
select {}
}
func sayhello(wr http.ResponseWriter, r *http.Request) {
KillOne()
}
func killThreadService() {
http.HandleFunc("/", sayhello)
err := http.ListenAndServe(":10003", nil)
if err != nil {
log.Fatal("ListenAndServe:", err)
}
}
// KillOne kills a thread
func KillOne() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
runtime.LockOSThread()
return
}()
wg.Wait()
}
启动后,发现创建了 1k+ 线程,curl localhost:10003,可以发现线程数在逐渐降低。