极端情况下收缩 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{}
}

threads

可见 Goroutine 退出了,历史上创建的线程也是不会退出的。之前我也一直认为没有办法退出这些线程,不过这周被同事教育,还是有办法的。

官方 issue

虽然问题直到现在依然没解决,但是这个 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,可以发现线程数在逐渐降低。

Xargin

Xargin

If you don't keep moving, you'll quickly fall behind
Beijing