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的设计理念将退出的调用权交给调用者
网友评论