Go语言协程使用

前言

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

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


协程的使用及控制

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


执行后输出


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

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

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

  • 通过sync. WaitGroup的三个方法 Add(), Done(), Wait() 来实现协程的控制
  • 通过带buffer的channel来控制
  • 通过sync. Cond

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



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



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




协程崩溃处理

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

  • recover如果想起作用的话, 必须在defered函数前声明,因为只要panic,后面的函数不会被执行
  • recover函数只有在方法内部发生panic时,返回值才不会为nil,没有panic的情况下返回值为nil

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


输出:


再看在panic前声明recover


输出


此时的panic被捕获了

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

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


输出


因此,如果在协程内执行其它函数时,为了保证不崩溃,安全的做法是,提前声明defer recover函数
这样可以保证协程内部崩溃,不会将整个进程崩溃掉


协程超时控制

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

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

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

代码示例如下


输出如下:


程序执行了超时退出


是否可以无限多开协程

众所周知,协程不同于线程,并不和操作系统的线程有具体的对应关系.协程是由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的使用,有利于更加准确的开发高效的并发处理程序.

全文完,感谢您的阅读.

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