美文网首页
2021/05/10 GO协程使用注意事项

2021/05/10 GO协程使用注意事项

作者: 温岭夹糕 | 来源:发表于2021-11-12 21:19 被阅读0次

1.前文知识点复习

1.在GO语句的执行规则中我们了解到了,一个系统线程M会对应一个GO程序的协程调度器P,P上保存着程序协程的队列信息
2.在GO的错误处理中,我们提到了要尽量避免创建"野生的协程",要学会"托管"我们委派的协程,"野生的协程"抛出panic会导致整个进程的挂掉

引出今天知识要点:
1.go关键字的使用注意事项
2.GO代码执行顺序

2.绝对不要在不不知协程生命周期的情况下创建协程

绝对不要在不不知协程生命周期的情况下创建协程,意味着我们在创建一个gorutine时需要注意;1.他啥时候会结束,2如何让他结束
不管理生命周期的demo

func main(){
   ch := make(chan int,3)
   go func(){ //1
      val:=<-ch
       fmt.Println(val)
    }()
    go func(){ // 2
       for {
          ch<-1
          time.Sleep(time.Second)
        }
    }()
   select {}//模拟阻塞
}

这里(2)不停的往通道里写数据,(1)只读取了1次,且读完之后并未通知2结束,造成了协程的泄露

2.1知道我们创建的协程会在什么时候结束

举一个例子,我们创建一个单接口的http服务,我们通常会这么写demo1(实际是引用官方文档net/http的demo):

func main(){
    http.HandleFunc("/",func(w http.ResponseWriter,r *http.Request){
        fmt.Println(w,"Hello,GopherCon SG")
    })
    if err := http.ListenAndServe(":8080",nil);err!=nil{
            log.Fatal(err)
    }
}

当我们的任务只有对外提供的主http服务(8080)时,这样写没什么错误,但是我们知道go中可以大量创建协程,想在这个程序中再开启一个端口来提供监听主http服务的哨兵服务(8081端口)时,我们可能会这么写demo2这里用http模拟哨兵服务阻塞监听

func main(){
    //本质上ServeMux只是一个路由管理器
    mux :=http.NewServeMux()
    mux.HandleFunc("/",func(w http.ResponseWriter,r *http.Request){
        fmt.Fprintln(w,"hello world")
    })
    go http.ListenAndServe("127.0.0.1:8081",http.DefaultServeMux) //1
    http.ListenAndServe("0.0.0.0:8080",mux)
}

这个时候我们就需要引起警惕了,我们在(1)位置go出去的代码什么时候会结束,换句话说他结束时会通知main主协程吗,很明显不能,那么造成的结果可能是,如果哨兵服务先退出,当我们的主服务异常退出时,我们想要从哨兵记录的日志中获取信息,却惊人的发现日志竟然丢失了,我们的程序出现了BUG
有种办法是利用log.Fatal来通知主协程demo4

go func(){
   if err:=http.ListenAndServe("127.0.0.1:8081",http.DefaultServeMux);err!=nil{
      log.Fatal(err)
}
}()

我们不鼓励这种写法,虽然log.Fatal会让main主协程一并退出解决上面出现的情况,但是log.Fatal实际是执行os.exit,让主协程强制结束不说,还不会触发go函数中的defer

2.2将并发的决定权交给调用者

上面的完整例子demo4

func main(){
    //我要一个退出整个退出使用log.Faltal
    go ServeDeBug()
    //不建议在servedebug内部使用go开启gorutine因为除非你是作者否则没人知道
    go ServeApp()
    select{} //模拟主协程还有其他工作
}

func ServeApp(){
err:=http.ListenAndServe("0.0.0.0:8080",http.DefaultServeMux);err!=nil{
        log.Fatal(err)
    }
}

func ServeDeBug(){
    if err:=http.ListenAndServe("127.0.0.1:8081",http.DefaultServeMux);err!=nil{
        log.Fatal(err)
    }
}

我们将服务各自封装为一个方法,这里我们为什么不在封装的方法内调用go关键字而是在main中调用go,如果你是作者,你知道哪些函数会委派协程那没关系随意写,但是如果你是第三方调用者,他(没阅读源码和注释)调用这个函数不知道这个函数不会阻塞,会创建协程会造成很多莫名其妙的"BUG",因此我们建议在设计函数api的时候将是否并发的调用权交给调用者

2.3如何让他主动结束

我们小结一下我们的需求:单个服务的退出会通知主协程,并让主协程决定何时退出
这里我们利用通道的特性demo5

func main(){
    done :=make(chan error,2)
    stop := make(chan struct{})
    go func(){
        done <- ServeApp(stop)
    }()
    go func(){
        done <- ServeDeBug(stop)
    }()

    var stopped bool
    for i:=0;i<cap(done);i++ {
        if err :=<-done;err!=nil{
            fmt.Println("error:%v",err)
        }
        //如果有一个报错就会调用close chan
        //同时唤醒serveDebug和serveApp的shutdown方法
        if !stopped {
            stopped = true
            close(stop)
        }
    }
}

func serve(addr string,handler http.Handler,stop <-chan struct{}) error {
    s := http.Server{
        Addr:addr,
        Handler:handler,
    }
    go func(){ //1
        <-stop
        //当收到停止信号时主动调用shutdown来平滑退出http服务
        s.Shutdown(context.Background() )
    }()
    return s.ListenAndServe()
}

func ServeApp(stop <-chan struct{}) error{
    return serve("0.0.0.0:8080",http.DefaultServeMux,stop,stop)
}

func ServeDeBug(stop <-chan struct{}) error{
    return serve("127.0.0.1:8081",http.DefaultServeMux,stop)
}

这里我们用done通道来接收两个服务的异常退出信号,stop通道用来主动结束其他服务协程(这个通道不写数据只为做信号量),一旦异常退出就关闭stop通道,就会触发(1)的shutdown函数来平滑退出http服务(shutdown底层实现了平滑退出)

这里我们是否也可以用context上下文来完全替代stop ??
阅读context源码发现
context.Context.Done()返回的是一个 <-struct{} 的通道
返回的cancel方法实际上就是关闭这个通道,那我们上面使用的stop思想不就是这个吗
context的简单使用demo6

func main(){
  ctx, cancel := context.WithCancel(context.Background())
  go test(ctx)
  time.Sleep(10 * time.Second)
    fmt.Println("可以了,通知监控停止")

    cancel()
    
    //为了检测监控过是否停止,如果没有监控输出,就表示停止了
    time.Sleep(5 * time.Second)
}

func test(ctx context.Context){
  go func(ctx context.Context){
    for {
      select {
      case <-ctx.Done():
        fmt.Println("子监控退出")
        return
      default :
        fmt.Println("子goroutine监控中...")
        time.Sleep(1 * time.Second)
      }
    }
  }(ctx)
  for {
    select {
    case <-ctx.Done():
      fmt.Println("监控退出")
      return
    default :
      fmt.Println("goroutine监控中...")
      time.Sleep(2 * time.Second)
    }
  }
}

2.4学会做超时控制

除了上面的提供阻塞服务的http服务,我们还可能会创建协程去查询数据库,假设某条sql的数据库写服务要阻塞很久,虽然说查到了就结束了,但这种情况本质上也是不知道什么协程时候会结束(假设阻塞很久),这时就要利用context做超时控制

2.5小结

无法保证创建的gorutine的生命周期的管理,就意味着结束服务时一些信息的丢失
我们利用error通道通知主协程让我们知道他啥时候会结束,我们利用关闭stop通道来达到让他结束的目的
同时我们也要注意将并发的决定权交给调用方

2.6小技巧扩展

上面的例子仅仅例举了服务在发生错误时结束的安全退出形式,那万一我们在发现到自己想要的资源时(比如目录管理系统,找到目标目录,假设目录很大没做索引需要完全遍历很耗时)想要主动退出结束时,除了使用类似关闭stop通道还能怎么实现?
遵循上面原则,还可以将退出的调用权交给调用者,即在设计函数时考虑传入调用者的回调函数
以path/filepath.WalkDir为例,先看一下函数签名

func WalkDir(root string,fn fs.WalkDirfunc) error
type WalkDirfunc func(path string,d DirEntry,err error) error

WalkDir遍历根目录下的文件树,为树中的每个文件或目录(包括根目录)调用fn。访问文件和目录时出现的所有错误都由fn过滤
WalkDir源码

func WalkDir(root string, fn fs.WalkDirFunc) error {
        info, err := os.Lstat(root)
        if err != nil {
                err = fn(root, nil, err)
        } else {
//这里遍历目录
                err = walkDir(root, &statDirEntry{info}, fn)
        }
        if err == SkipDir {
                return nil
        }
        return err
}

walkDIr源码

func walkDir(path string, d fs.DirEntry, walkDirFn fs.WalkDirFunc) error {
//当我们找到想要的路径时可以主动抛出异常停止迭代
        if err := walkDirFn(path, d, nil); err != nil || !d.IsDir() {
                if err == SkipDir && d.IsDir() {
                        // Successfully skipped directory.
                        err = nil
                }
                return err
        }

        dirs, err := readDir(path)
        if err != nil {
                // Second call, to report ReadDir error.
                err = walkDirFn(path, d, err)
                if err != nil {
                        return err
                }
        }

        for _, d1 := range dirs {
                path1 := Join(path, d1.Name())
                if err := walkDir(path1, d1, walkDirFn); err != nil {
                        if err == SkipDir {
                                break
                        }
                        return err
                }
        }
        return nil
}

我们可以参考WalkDir的设计理念将退出的调用权交给调用者

相关文章

网友评论

      本文标题:2021/05/10 GO协程使用注意事项

      本文链接:https://www.haomeiwen.com/subject/aipbdltx.html