工作中查看项目代码,发现会存在使用 GO 语言做并发的时候出现各种各样的异常情况,有的输出结果和自己期望和设计的不一致,有的是程序直接阻塞住,更有甚者直接是程序 crash 掉。
实际上,出现上述的情况,还是因为我们对于 GO 语言的并发模型和涉及的 GO 语言基础不够扎实,误解了语言的用法。
那么,对于 GO 语言的并发模式,我们一起来梳理一波。 GO 语言常见的并发模式有这些:
在 GO 语言里面,咱们使用使用并发,自然离不开使用 GO 语言的协程 goroutine,通道 channel 和 多路复用 select,接下来就来看看各种模式都是如何去搭配使用这三个关键原语的
使用过通道和协程的朋友对于创建模式肯定不会模式,这是一个非常常用的方式,也是一个非常简单的使用方式:
type XXX struct{...}
func help(fn func()) chan XXX {
ch := make(chan XXX)
// 开启一个协程
go func(){
// 此处的协程可以控制和外部的 主协程 通过 ch 来进行通信,达到一定逻辑便可以执行自己的 fn 函数
fn()
ch <- XXX
}()
}
func main(){
ch := help(func(){
fmt.Println("这是GO 语言 并发模式之 创建模式")
})
<- ch
}
程序的退出我们应该也不会陌生,对于一些常驻的服务,如果是要退出程序,自然是不能直接就断掉,此时会有一些连接和业务并没有关闭,直接关闭程序会导致业务异常,例如在关闭过程中最后一个 http 请求没有正常响应等等等
此时,就需要做优雅关闭了,对于协程 goroutine 退出有 3 种模式
此处的分离模式,分离这个术语实际上是线程中的术语,pthread detached
分离模式可以理解为,咱们创建的协程 goroutine,直接分离,创建子协程的父协程不用关心子协程是如何退出的,子协程的生命周期主要与它执行的主函数有关,咱们 return 之后,子协程也就结束了
对于这类分离模式的协程,咱们需要关注两类,一种是一次性的任务,咱们 go 出来后,执行简单任务完毕后直接退出,一种是常驻程序,需要优雅退出,处理一些垃圾回收的事情
例如这样:
func main(){
ch := make(chan os.Signal)
signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
// ...
// go 程序执行其他业务
// ...
for i := range ch {
switch i {
case syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT:
// 做一些清理工作
os.Exit(0)
}
}
}
看到这个关键字,是不是也似曾相识,和线程貌似很像,例如 线程中 父线程可以通过 pthread_join 来等待子线程结束,并且还可以获取子线程的结束状态
GO 语言中等待子协程退出并且获取子协程的退出状态,咱们就可以使用通道 channel 的方式来进行处理
例子1
等待一个子协程退出,并获取退出状态
func help(f func(bool) error, ok bool) <-chan error {
ch := make(chan error)
go func() {
ch <- f(ok)
}()
return ch
}
func fn(ok bool) error {
if !ok {
return errors.New("not ok ... ")
}
return nil
}
func main() {
ch := help(fn, true)
fmt.Println("help 111")
err := <-ch
fmt.Println("help 111 done ", err)
ch = help(fn, false)
fmt.Println("help 222")
err = <-ch
fmt.Println("help 222 done ", err)
}
看上如上程序,我们就可以知道,第一次调用 help(fn , true)
,主协程等待子协程退出的时候,会得到一个错误信息,为 not ok ...
, 第二次调用 help(fn , false)
的时候,返回的 err 是一个 nil
通过上述这种方式,主协程不仅可以轻易的等待一个子协程退出,还可以获取到子协程退出的状态
那么,主协程如果是等待多个协程退出呢?需要如何处理?
例子2
主协程等待多个协程退出咱们就需要使用到 GO 中的 sync.WaitGroup
func help(f func(bool)error, ok bool, num int)chan struct{}{
ch := make(chan struct{})
var wg sync.WaitGroup
for i:=0; i
可以看到上述模式,都是主协程等待一个子协程,或者多个子协程结束后,主协程再进行退出,或者处理完垃圾回收后退出
那么如果主协程要主动通知子协程退出,我们应该要如何处理呢?
同样的问题,如果主协程自己退出了,而没有通知其他子协程退出,这是会导致业务数据异常或者丢失的,那么此刻我们就可以使用到 notify-and-wait 模式 来进行处理
我们就直接来写一个主协程通知并等待多个子协程退出的 demo:
func fn(){
// 模拟在处理业务
time.Sleep(time.Second * 1)
}
func help(num int, f func()) chan struct{}{
quit := make(chan struct{})
j := make(chan int)
var wg sync.WaitGroup
// 创建子协程处理业务
for i:=0;i
上述程序执行结果如下,可以看到 help 函数创建了 10 个子协程,主协程主动通知子协程全部退出,退出的时候也是 10 个子协程退出了,主协程才退出
上述程序,如果某一个子协程出现了问题,导致子协程不能完全退出,也就是说某些子协程在 f 函数中阻塞住了,那么这个时候主协程岂不是一直无法退出???
那么此时,在主协程通知子协程退出的时候,我们加上一个超时时间,表达意思为,超过某个时间,如果子协程还没有全部退出完毕,那么主协程仍然主动关闭程序,可以这样写:
t := time.NewTimer(time.Second * 3)
defer t.Stop()
// 此处等待所有子程序退出
select{
case <-t.C:
fmt.Println("timeout programs exit. ")
case <- quit:
fmt.Println(" 111 programs exit. ")
}
说到管理,或许大家对 linux 里面的管道更加熟悉吧,例如使用 linux 命令找到文件中的 golang 这个字符串
cat xxx.txt |grep "golang"
那么对于 GO 语言并发模式中的管道模式也是类似的效果,我们就可以用这个管道模式来过滤数据
例如我们可以设计这样一个程序,兄弟们可以动起手来写一写,评论区见哦:
管道模式有两种模式,扇出模式 和 扇入模式,这个比较好理解
上述例子中有专门说到如何去使用他,实际上我们还可以这样用:
select{
case <- time.Afer(time.Second * 2):
fmt.Println("timeout programs exit. ")
case <- quit:
fmt.Println(" 111 programs exit. ")
}
则是使用了 GO 语言的 context 包中的提供了上下文机制,可以在协程 goroutine 之间传递 deadline,取消等信号
我们使用的时候例如可以这样:
ctx, cancelFunc := context.WithCancel(context.Background())
go func() {
time.Sleep(time.Second * 3)
cancelFunc()
}()
for {
select {
case <-ctx.Done():
fmt.Println("program exit .")
return
default:
fmt.Println("I'm still here.")
time.Sleep(time.Second)
}
}
总的来说,今天分享了 GO 语言中常见的几种并发模式:创建模式,退出模式,管道模式,超时模式和取消模式,更多的,还是要我们要思考其原理和应用起来,学习他们才能更加的有效
朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力
好了,本次就到这里
技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。
我是阿兵云原生,欢迎点赞关注收藏,下次见~
可以进入地址进行体验和学习:https://xxetb.xet.tech/s/3lucCI