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的使用,有利于更加准确的开发高效的并发处理程序.
全文完,感谢您的阅读.
如有不正确的地方,欢迎留言讨论.