Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > go并发模式

Go语言中的并发模式你了解了吗

作者:阿兵云原生

工作中查看项目代码,发现会存在使用 GO 语言做并发的时候出现各种各样的异常情况,实际上,出现上述的情况,还是因为我们对于 GO 语言的并发模型和涉及的 GO 语言基础不够扎实,所以本文小编就来带大家深入了解下Go语言中的并发模式吧

工作中查看项目代码,发现会存在使用 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)
        }
    }
}

join 模式

看到这个关键字,是不是也似曾相识,和线程貌似很像,例如 线程中 父线程可以通过 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<num; i++ {
        wg.Add(1)
        go func(){
            f(ok)
            fmt.Println(" f done ")
            wg.Done()
        }() 
    }
    go func(){
        // 等待所有子协程退出
        wg.Wait()
        ch <- struct{}{}
    }()
    return ch
}
func fn(ok bool) error{
    time.Sleep(time.Second * 1)
    if !ok{
        return errors.New("not ok ... ")
    }
    return nil
}
func main(){
    ch := help(fn , true)
    fmt.Println("help 111")
     <- ch 
    fmt.Println("help 111 done ",err)
}

notify-and-wait 模式

可以看到上述模式,都是主协程等待一个子协程,或者多个子协程结束后,主协程再进行退出,或者处理完垃圾回收后退出

那么如果主协程要主动通知子协程退出,我们应该要如何处理呢?

同样的问题,如果主协程自己退出了,而没有通知其他子协程退出,这是会导致业务数据异常或者丢失的,那么此刻我们就可以使用到 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<num;i++{
      wg.Add(1)
      go func(){
         defer wg.Done()
         _,ok:=<-j
         if !ok{
            fmt.Println("exit child goroutine .")
            return
         }
         // 子协程 正常执行业务
         f()
      }()
   }
   go func(){
      <-quit
      close(j)
      // 等待子协程全部退出
      wg.Wait()
      quit <- struct{}{}
   }()
   return quit
}
func main(){
   quit := help(10, fn)
   // 模拟主程序处理在处理其他事项
   // ...
   time.Sleep(time.Second * 10)
   quit <- struct{}{}
   // 此处等待所有子程序退出
   select{
   case <- quit:
      fmt.Println(" programs exit. ")
   }
}

上述程序执行结果如下,可以看到 help 函数创建了 10 个子协程,主协程主动通知子协程全部退出,退出的时候也是 10 个子协程退出了,主协程才退出

上述程序,如果某一个子协程出现了问题,导致子协程不能完全退出,也就是说某些子协程在 f 函数中阻塞住了,那么这个时候主协程岂不是一直无法退出???

那么此时,在主协程通知子协程退出的时候,我们加上一个超时时间,表达意思为,超过某个时间,如果子协程还没有全部退出完毕,那么主协程仍然主动关闭程序,可以这样写:

设定一个定时器, 3 秒后会触发,即可以从 t.C 中读取到数据

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 语言中常见的几种并发模式:创建模式,退出模式,管道模式,超时模式和取消模式,更多的,还是要我们要思考其原理和应用起来,学习他们才能更加的有效

以上就是Go语言中的并发模式你了解了吗的详细内容,更多关于go并发模式的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文