美文网首页
一文搞懂go语言goroutine使用

一文搞懂go语言goroutine使用

作者: xcrossed | 来源:发表于2021-05-22 22:43 被阅读0次

    go语言协程使用

    前言

    协程的使用及控制

    协程崩溃处理

    协程超时控制

    是否可以无限多开协程

    高并发下情况下如何开协程

    结尾

    go语言协程使用

    前言

    go语言的真正精髓,莫过于go协程和channel.因此对于goroutine和channel的正确使用,非常重要.是编写高并发程序的重要基础.本文试图将协程的使用方方面面讲清楚.

    全文阅读大概需要30分钟.如时间不够,欢迎收藏后阅读.

    协程的使用及控制

    在go语言中开一个协程非常方便,在需要通过协程来执行的函数时,直接在函数前加go关键字就可以

    packagemainimport("fmt")funcA(iint){fmt.Println("我是A")}funcmain(){fmt.Println("我是main")goA(1)fmt.Println("执行完了")}

    执行后输出

    我是main

    执行完了

    程序正常执行没有报错,但是没有函数A的输出.

    这是因为主协程并不会等待子协程执行完才往下走,执行到go后,语句会继续执行,go后面的函数新开一个协程,各跑各的,所以主协程执行完go语句,就无事可做,就退出了.

    那怎么让上面的代码打印出函数A的输出.得让主函数待一会儿协程执行完了再退出,或者让主协程不退出,比如在web程序中,主协程是不退出的.

    通过sync. WaitGroup的三个方法 Add()Done()Wait() 来实现协程的控制

    通过带buffer的channel来控制

    通过sync. Cond

    下面演示下等待协程完成的代码

    packagemainimport("fmt""sync")funcA(iint){fmt.Println("我是A",i)}funcmain(){varwg sync.WaitGroupfmt.Println("我是main")wg.Add(1)gofunc(iint){deferwg.Done()A(i)}(1)wg.Wait()fmt.Println("执行完了")}

    我是main我是A1执行完了

    下面演示通过channel来控制协程的流程

    packagemainimport("fmt")funcA(iint){fmt.Println("我是A",i)}funcmain(){ch:=make(chanbool,1)fmt.Println("我是main")gofunc(iint,chpchan<-bool){deferclose(chp)A(i)fmt.Println("finish")chp<-true}(1,ch)fmt.Println("wait")<-chfmt.Println("执行完了")}

    我是mainwait我是A1finish执行完

    下面演示通过sync. Cond来实现

    packagemainimport("fmt""sync")funcA(iint){fmt.Println("我是A",i)}funcmain(){varlocker=new(sync.Mutex)varcond=sync.NewCond(locker)vardonebool=falsefmt.Println("我是main")cond.L.Lock()gofunc(iint){A(i)fmt.Println("finish")done=truecond.Signal()}(1)fmt.Println("wait")if!done{cond.Wait()cond.L.Unlock()}fmt.Println("执行完了")}

    我是mainwait我是A1finish执行完了

    代码示例:https://play.studygolang.com/p/y-ushG8zVRP

    协程崩溃处理

    在go语言中,如果一个协程崩溃了,则所有协程都会退出,比如数组越界,会触发panic(相当于throw exception), 这对持续可运行的应用来说,显然不是我们想要的效果.那这个时候我们需要对崩溃进行修复.在go语言中提供了一个defer和recover来实现崩溃恢复,这个相当于其它语言的try catch的方式.

    在使用recover函数时,如果要达到能捕获异常的作用,有几点需要注意:

    recover如果想起作用的话, 必须在defered函数前声明,因为只要panic,后面的函数不会被执行

    recover函数只有在方法内部发生panic时,返回值才不会为nil,没有panic的情况下返回值为nil

    下面用代码示例来说明情况

    packagemainimport("fmt""sync")funcA(iint){fmt.Println("我是A",i)panic("崩溃")deferfunc(){//在panic后声明defer,不能捕获异常iferr:=recover();err!=nil{fmt.Println("恢复",err)}}()}funcmain(){varwg sync.WaitGroupfmt.Println("我是main")wg.Add(1)gofunc(iint){deferwg.Done()A(i)}(1)wg.Wait()fmt.Println("执行完了")}

    输出:

    ./prog.go:11:2: unreachable codeGo vet exited.我是main我是A1panic: 崩溃goroutine6[running]:main.A(0x1)/tmp/sandbox516871981/prog.go:10 +0xc5main.main.func1(0xc000018040, 0x1)/tmp/sandbox516871981/prog.go:24 +0x53created by main.main/tmp/sandbox516871981/prog.go:22 +0xd5

    再看在panic前声明recover

    packagemainimport("fmt""sync")funcA(iint){deferfunc(){//在panic前声明defer,能捕获异常iferr:=recover();err!=nil{fmt.Println("恢复",err)}}()fmt.Println("我是A",i)panic("崩溃")}funcmain(){varwg sync.WaitGroupfmt.Println("我是main")wg.Add(1)gofunc(iint){deferwg.Done()A(i)}(1)wg.Wait()fmt.Println("执行完了")}

    输出

    我是main我是A1恢复 崩溃执行完了

    此时的panic被捕获了

    defer recover函数必须放在需要捕获panic的函数前面

    因此本示例如果将defer recover放在go func函数中被调用函数f前面,也能捕获住A函数的panic

    packagemainimport("fmt""sync")funcA(iint){fmt.Println("我是A",i)panic("崩溃")}funcmain(){varwg sync.WaitGroupfmt.Println("我是main")wg.Add(1)gofunc(iint){deferfunc(){//在调用A函数前声明defer recover,能捕获异常iferr:=recover();err!=nil{fmt.Println("恢复",err)}wg.Done()}()A(i)}(1)wg.Wait()fmt.Println("执行完了")}

    输出

    我是main我是A1恢复 崩溃执行完了

    因此,如果在协程内执行其它函数时,为了保证不崩溃,安全的做法是,提前声明defer recover函数

    这样可以保证协程内部崩溃,不会将整个进程崩溃掉

    协程超时控制

    当你希望控制一个协程的执行时间,如果超过指定时间,还没有执行完,则退出.直接返回超时错误,这个该如何做呢?

    通行做法是用select + channel来进行超时控制,

    channel发执行完毕的信号,然后超时信号通用ctx. Done()或者time. After(), 或者time. Ticket()来完成超时通知退出,select捕获到其中一个channel有数据,就执行对应的代码,然后退出.

    其中且个注意的点是, channel要用有缓冲的,不然,在超时分支退出时,协程还在卡住,造成goroutine泄露.

    代码示例如下

    packagemainimport("context""fmt""sync""time")funcDo(ctx context.Context,wg*sync.WaitGroup){ctx,cancle:=context.WithTimeout(ctx,time.Second*2)deferfunc(){cancle()wg.Done()}()done:=make(chanstruct{},1)//执行成功的channelgofunc(ctx context.Context){fmt.Println("go goroutine")time.Sleep(time.Second*10)done<-struct{}{}//发送完成的信号}(ctx)select{case<-ctx.Done()://超时fmt.Printf("timeout,err:%v\n",ctx.Err())case<-time.After(3*time.Second)://超时第二种方法fmt.Printf("after 1 sec.")case<-done://程序正常结束fmt.Println("done")}}funcmain(){fmt.Println("main")ctx:=context.Background()varwg sync.WaitGroupwg.Add(1)Do(ctx,&wg)wg.Wait()fmt.Println("finish")}

    输出如下:

    main

    go goroutine

    timeout,err:context deadline exceeded

    finish

    程序执行了超时退出

    是否可以无限多开协程

    众所周知,协程不同于线程,并不和操作系统的线程有具体的对应关系.协程是由go的一个线程池来调度的.

    go runtime并不会产生一个协程对应产生一个os线程,是一个m:n的对应关系,根据m:n对应关系,协程对应的os线程runtime. GOMAXPROCS默认为系统逻辑cpu数量,因此创建更多的m并不会产生更多的操作系统线程,但是可以通过runtime. GOMAXPROCS()来设置当前程序运行时占用的系统核心数

    协程创建需要占用一定量的内存,开一个协程只需要少量的内存空间,几KB,这也是golang能实现百万长链的原因.

    但在实际中,协程需要正确的关闭,而不是无限创建后,造成协程泄露,进而引发系统崩溃.

    高并发下情况下如何开协程

    在高并发情况下,需要通过一个带缓冲的channel的来实现对于协程的创建数量进行控制,进而实现一个健康稳定的可持续运行的高并发处理程序.

    结尾

    现代语言的发展,从进程到线程,进而到协程.各种语言的诞生,一直致力于2个方面的努力.

    更高效率的利用CPU

    更低成本的实现并发

    目前来看,go语言的协程则是这2个思路的最佳实践.go的协程和channel是go语言的真正精髓.掌握好go+channel的使用,有利于更加准确的开发高效的并发处理程序.

    全文完,感谢您的阅读.

    如有不正确的地方,欢迎留言讨论.

    相关文章

      网友评论

          本文标题:一文搞懂go语言goroutine使用

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