文章目录
前言
项目选择Go通常是因为它的并发特性。Go 团队已经花了很大的精力使 Go 中的并发性 变得廉价(就硬件资源而言)和高效,但是使用 Go 的并发特性来编写代码是可能的,这既不可靠也不可行。我想留给你一些建议,以避免Go的并发特性带来的一些陷阱
提示:以下是本篇文章正文内容,下面案例可供参考
一、让自己忙碌起来或者自己完成工作
英文:Keep yourself busy or do the work yourself
我们看一下第一个例子
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello")
})
//开启一个 协程 处理http的监听
go func() {
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err) // 如果出现错误立即退出
}
}()
//for 死循环
for {
}
}
这是一个简单的http的服务器,for 循环 在无限的浪费 CPU的资源
二、将并发 或者 (选择权)留给 调用者
1.例子 一
func debug(){
go http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux)
}
func app(){
go func() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
fmt.Fprintln(resp, "Hello, QCon!")
})
http.ListenAndServe("0.0.0.0:8080", mux)
}()
}
func main() {
debug() // debug
app() // app traffic
select{}
}
看上面的这个例子
开起了两个监听服务,8080是线上访问,8001 的debug
问题1.线上出现事故想要调试结果 debug 不知道什么时候退出了。这个时候就傻x 了?
问题2.main 函数进行了 一个 select{} 是不是一直阻塞 跟个傻x 一样!
问题3.如果说这两个服务在你的其他包里,也不写注释,其他人会发现这是一个 协程吗?
例子 二
func debug(){
http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux)
}
func app(){
mux := http.NewServeMux()
mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
fmt.Fprintln(resp, "Hello, QCon!")
})
http.ListenAndServe("0.0.0.0:8080", mux)
}
func main() {
go debug() // debug
go app() // app traffic
select{}
}
这个例子 遵循了 将并发留给调用者
正如 Go 中的函数将并发性留给调用方一样,应用程序应该将监视其状态的工作留给调用它们的程序,如果调用失败,应用程序应该重新启动它们。不要让您的应用程序自己负责重新启动,这是一个最好从应用程序外部处理的过程
问题1.线上出现事故想要调试结果 debug 不知道什么时候退出了。这个时候就傻x 了?
问题2.main 函数进行了 一个 select{} 是不是一直阻塞 会显得很尴尬
例子三
func debug(){
if err := http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux); err != nil{
log.Fatal(err)
}
}
func app(){
mux := http.NewServeMux()
mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
fmt.Fprintln(resp, "Hello, QCon!")
})
if err := http.ListenAndServe("0.0.0.0:8080", mux); err != nil {
log.Fatal(err)
}
}
func main() {
go debug() // debug
go app() // app traffic
select{}
}
这个例子 如果两个监听其他一个返回err 就立即终止了。
我们真正想要的是将发生的任何错误传递回 goroutine 的发起者,这样它就可以知道 goroutine 为什么停止,可以干净地关闭进程
存在的问题
1.log.fatal 无法被捕获到的,main 函数无从得知。
2.select 还是一直在等待
三.在不知道什么时候会停止的情况下,不要开始一个goroutine
例子三
func serve(addr string, handler http.Handler, stop <-chan struct{}) error {
s := http.Server{
Addr: addr,
Handler: handler,
}
//开启一个协程接受 关闭信号 进行关闭
go func() {
<-stop // wait for stop signal
s.Shutdown(context.Background())
}()
return s.ListenAndServe()
}
func serveApp(stop <-chan struct{}) error {
mux := http.NewServeMux()
mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
fmt.Fprintln(resp, "Hello, QCon!")
})
return serve("0.0.0.0:8080", mux, stop)
}
func serveDebug(stop <-chan struct{}) error {
return serve("127.0.0.1:8001", http.DefaultServeMux, stop)
}
func main() {
//done 记录返回的错误
done := make(chan error, 2)
//定义终止信号
stop := make(chan struct{})
//开启 debug
go func() {
done <- serveDebug(stop)
}()
//开启主程序
go func() {
done <- serveApp(stop)
}()
var stopped bool //只记录1次
//因为有两个协程 所有循环2
for i := 0; i < cap(done); i++ {
//当其中一个返回error 时 会打印错误
if err := <-done; err != nil {
fmt.Println("error: %v", err)
}
//并通知协助进行关闭
if !stopped {
stopped = true
close(stop)
}
}
}
每次我们在 done 通道上接收到一个值,我们就关闭 stop 通道,这会导致所有 goroutine 等待该通道关闭它们的 http。这反过来会导致所有剩余的列表和服务 goroutine 返回。一旦我们启动的所有 goroutine 都停止了,main.main 返回并且进程完全停止。
这个代码还是有一些缺陷的,server 里没有进行 错误捕获 Shutdown的错误 还是有可能停止失败的
四 goroutine 泄露
例子一
package main
import (
"context"
"fmt"
"net/http"
"net/http/pprof"
)
func serve(addr string, handler http.Handler, stop <-chan struct{}) error {
s := http.Server{
Addr: addr,
Handler: handler,
}
//开启一个协程接受 关闭信号 进行关闭
go func() {
<-stop // wait for stop signal
s.Shutdown(context.Background())
}()
return s.ListenAndServe()
}
func serveApp(stop <-chan struct{}) error {
mux := http.NewServeMux()
mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
fmt.Fprintln(resp, "Hello, QCon!")
})
mux.HandleFunc("/received", func(resp http.ResponseWriter, req *http.Request) {
//泄露代码
ch := make(chan int)
//这个协程一直在等待 ch 发送的值 但是没有人会发 造成泄露
go func() {
val := <-ch
fmt.Println("We received a value:", val)
}()
//请求这个链接会直接返回
fmt.Fprintln(resp, "Hello, received!")
})
return serve(":8080", mux, stop)
}
func serveDebug(stop <-chan struct{}) error {
pprofHandler := http.NewServeMux()
pprofHandler.Handle("/debug/pprof/", http.HandlerFunc(pprof.Index))
return serve(":8001", pprofHandler, stop)
}
func main() {
//done 记录返回的错误
done := make(chan error, 2)
//定义终止信号
stop := make(chan struct{})
//开启 debug
go func() {
done <- serveDebug(stop)
}()
//开启主程序
go func() {
done <- serveApp(stop)
}()
var stopped bool //只记录1次
//因为有两个协程 所有循环2
for i := 0; i < cap(done); i++ {
//当其中一个返回error 时 会打印错误
if err := <-done; err != nil {
fmt.Println("error: %v", err)
}
//并通知协助进行关闭
if !stopped {
stopped = true
close(stop)
}
}
}
/received 请求这个url 测试泄露
下面是,请求这个链接多次 和最初的对比
没有请求这个url时的协程数量 只有8个
但是多请求几次就会出现 很多 协程无法退出 造成泄露
确保创建出的 goroutine 的工作已经完成
package main
import (
"context"
"fmt"
"net/http"
"net/http/pprof"
"time"
)
func serve(addr string, handler http.Handler, stop <-chan struct{}) error {
s := http.Server{
Addr: addr,
Handler: handler,
}
//开启一个协程接受 关闭信号 进行关闭
go func() {
<-stop // wait for stop signal
s.Shutdown(context.Background())
}()
return s.ListenAndServe()
}
func serveApp(stop <-chan struct{}) error {
mux := http.NewServeMux()
mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
fmt.Fprintln(resp, "Hello, QCon!")
})
mux.HandleFunc("/received", func(resp http.ResponseWriter, req *http.Request) {
//泄露代码
ch := make(chan int)
//这个协程一直在等待 ch 发送的值 但是没有人会发 造成泄露
go func() {
val := <-ch
fmt.Println("We received a value:", val)
}()
//请求这个链接会直接返回
fmt.Fprintln(resp, "Hello, received!")
})
mux.HandleFunc("/index", func(resp http.ResponseWriter, req *http.Request) {
go func() {
time.Sleep(time.Second * 5) //进行上报信息
fmt.Println("请求上报信息")
}()
//请求这个链接会直接返回
fmt.Fprintln(resp, "Hello, index!")
})
return serve(":8080", mux, stop)
}
func serveDebug(stop <-chan struct{}) error {
pprofHandler := http.NewServeMux()
pprofHandler.Handle("/debug/pprof/", http.HandlerFunc(pprof.Index))
return serve(":8001", pprofHandler, stop)
}
func main() {
//done 记录返回的错误
done := make(chan error, 2)
//定义终止信号
stop := make(chan struct{})
//开启 debug
go func() {
done <- serveDebug(stop)
}()
//开启主程序
go func() {
done <- serveApp(stop)
}()
var stopped bool //只记录1次
//因为有两个协程 所有循环2
for i := 0; i < cap(done); i++ {
//当其中一个返回error 时 会打印错误
if err := <-done; err != nil {
fmt.Println("error: %v", err)
}
//并通知协助进行关闭
if !stopped {
stopped = true
close(stop)
}
}
}
这个例子加了一个index 的url 里面进行的一些上报信息的操作
问题1.如果这个上报一直等待 就会造成泄露
问题2.你永远也不知道什么时候停止
总结
- 请将是否并发调用的选择权交给调用者。
- 请你对开启的协程负责。2.1 永远不要开启一个你不知道什么时候退出的协程。2.2要管控这个协程的生命周期 知道什么嘛时候退出。不管是chen 还是 context
- 尽量避免在请求中开启协程,上报的执行任务可以投递到消息队列或者自己定义worker里避免协程的泄露
一些大佬的文献
Practical Go: Real world advice for writing maintainable Go programs
Concurrency Trap #2: Incomplete Work
https://www.ardanlabs.com/blog/2018/11/goroutine-leaks-the-forgotten-sender.html
https://www.ardanlabs.com/blog/2014/01/concurrency-goroutines-and-gomaxprocs.html