Go编程语言一书示例中的goroutine泄漏
我正在阅读 The Go Programming Language 一书,书中有一个例子说明了 goroutine 泄漏
func mirroredQuery() string {
responses := make(chan string, 3)
go func() { responses <- request("asia.gopl.io") }()
go func() { responses <- request("europe.gopl.io") }()
go func() { responses <- request("americas.gopl.io") }()
return <-responses // return the quickest response
}
func request(hostname string) (response string) { /* ... */ }
我试图解决泄漏,并得到以下代码
func request(url string) string {
res, err := http.Get(url)
if err == nil {
body, err := io.ReadAll(res.Body)
if err == nil {
return string(body)
} else {
return err.Error()
}
} else {
return err.Error()
}
}
func getany() string {
rsp := make(chan string, 3)
done := make(chan struct{}, 3)
doRequest := func(url string) {
select {
case rsp <- request(url):
fmt.Printf("get %sn", url)
done <- struct{}{}
case <- done:
fmt.Printf("stop %sn", url)
return
}
}
go doRequest("http://google.com")
go doRequest("http://qq.com")
go doRequest("http://baidu.com")
return <-rsp
}
但似乎并没有解决问题?有什么建议?
回答
提供的代码中没有 goroutine 泄漏。该mirroredQuery方法使用缓冲通道来收集结果并返回第一个答案。并且当前缓冲区有足够的空间来收集所有 goroutine 的所有答案,即使其余的响应从未被读取。如果缓冲区小于 N - 1,情况就会改变,其中 N 是生成的 goroutine 的数量。在这种情况下,一些生成的 goroutinemirroredQuery会在尝试向responses通道发送响应时卡住。重复调用mirroredQuery会导致卡住的 goroutines 增加,可以称为 goroutines 泄漏。
这是添加了日志的代码和两种方案的输出。
func mirroredQuery() string {
responses := make(chan string, 2)
go func() {
responses <- request("asia.gopl.io")
log.Printf("Finished goroutine asia.gopl.ion")
}()
go func() {
responses <- request("europe.gopl.io")
log.Printf("Finished goroutine europe.gopl.ion")
}()
go func() {
responses <- request("americas.gopl.io")
log.Printf("Finished goroutine americas.gopl.ion")
}()
return <-responses // return the quickest response
}
func request(hostname string) (response string) {
duration := time.Duration(rand.Int63n(5000)) * time.Millisecond
time.Sleep(duration)
return hostname
}
func main() {
rand.Seed(time.Now().UnixNano())
result := mirroredQuery()
log.Printf("Fastest result for %sn", result)
time.Sleep(6*time.Second)
}
缓冲区大小 >= N-1 的输出
2021/06/26 16:05:27 Finished europe.gopl.io
2021/06/26 16:05:27 Fastest result for europe.gopl.io
2021/06/26 16:05:28 Finished asia.gopl.io
2021/06/26 16:05:30 Finished americas.gopl.io
Process finished with the exit code 0
缓冲区大小 < N-1 的输出
2021/06/26 15:47:54 Finished europe.gopl.io
2021/06/26 15:47:54 Fastest result for europe.gopl.io
Process finished with the exit code 0
上面的实现可以通过在第一个响应到达时引入 goroutines 终止来“改进”。这可能会减少已用资源的数量。这在很大程度上取决于request做什么方法。对于计算量大的场景来说,这是有道理的,取消 http 请求可能会导致连接终止,因此下一个请求必须打开新的请求。对于高负载的服务器,即使不使用响应,它也可能不如等待响应有效。
下面是改进的context使用实现。
func mirroredQuery() string {
responses := make(chan string, 2)
go func() {
responses <- request("asia.gopl.io")
log.Printf("Finished goroutine asia.gopl.ion")
}()
go func() {
responses <- request("europe.gopl.io")
log.Printf("Finished goroutine europe.gopl.ion")
}()
go func() {
responses <- request("americas.gopl.io")
log.Printf("Finished goroutine americas.gopl.ion")
}()
return <-responses // return the quickest response
}
func request(hostname string) (response string) {
duration := time.Duration(rand.Int63n(5000)) * time.Millisecond
time.Sleep(duration)
return hostname
}
func main() {
rand.Seed(time.Now().UnixNano())
result := mirroredQuery()
log.Printf("Fastest result for %sn", result)
time.Sleep(6*time.Second)
}