GO 语言的并发模式你了解多少?

工作中查看项目代码,发现会存在使用 GO 语言做并发的时候出现各种各样的异常情况,有的输出结果和自己期望和设计的不一致,有的是程序直接阻塞住,更有甚者直接是程序 crash 掉。

实际上,出现上述的情况,还是因为我们对于 GO 语言的并发模型和涉及的 GO 语言基础不够扎实,误解了语言的用法。

那么,对于 GO 语言的并发模式,我们一起来梳理一波。 GO 语言常见的并发模式有这些:

  1. 创建模式
  2. 退出模式
  3. 管道模式
  4. 超时模式和取消模式

在 GO 语言里面,咱们使用使用并发,自然离不开使用 GO 语言的协程 goroutine,通道 channel 和 多路复用 select,接下来就来看看各种模式都是如何去搭配使用这三个关键原语的

创建模式

使用过通道和协程的朋友对于创建模式肯定不会模式,这是一个非常常用的方式,也是一个非常简单的使用方式:

  1. 主协程中调用 help 函数,返回一个通道 ch 变量
  2. 通道 ch 用于主协程和 子协程之间的通信,其中通道的数据类型完全可以自行定义
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 种模式

  1. 分离模式
  2. join 模式
  3. notify-and-wait 模式

分离模式

此处的分离模式,分离这个术语实际上是线程中的术语,pthread detached

分离模式可以理解为,咱们创建的协程 goroutine,直接分离,创建子协程的父协程不用关心子协程是如何退出的,子协程的生命周期主要与它执行的主函数有关,咱们 return 之后,子协程也就结束了

对于这类分离模式的协程,咱们需要关注两类,一种是一次性的任务,咱们 go 出来后,执行简单任务完毕后直接退出,一种是常驻程序,需要优雅退出,处理一些垃圾回收的事情

例如这样:

  1. 主程序中设置一个通道变量 ch ,类型为 os.Signal
  2. 然后主程序就开始各种创建协程执行自己的各种业务
  3. 直到程序收到了 syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT 任意一个信号的时候,则会开始进行垃圾回收等清理工作,执行完毕后,程序再进行退出
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

等待一个子协程退出,并获取退出状态

  1. 主协程中调用 help 方法得到一个 ch 通道变量,主协程阻塞着读 ch
  2. help 中开辟一个子协程去执行传入的 fn 回调函数,并传参为 ok bool
  3. 实际 fn 函数判断传参 ok 是否是 true,若不是则返回具体的错误信息,若是 true 则返回 nil
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

  1. 使用 help 函数,传入回调函数,参数1 bool,参数2 int ,其中参数 2 表示开辟子协程的个数,返回值为一个无缓冲的 channel 变量,数据类型是 struct{}
  2. 使用 var wg sync.WaitGroup ,开辟子协程的时候记录一次 wg.Add(1),当子协程退出时 ,记录退出 wg.Done()
  3. help 中再另起一个协程 wg.Wait() 等待所有子协程退出,并将 ch 变量写入值
  4. 主协程阻塞读取 ch 变量的值,待所有子协程都退出之后,help 中写入到 ch 中的数据,主协程就能马上收到 ch 中的数据,并退出程序
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:

  1. 主协程调用 help 函数,得到一个 quit chan struct{} 类型的通道变量,主协程阻塞读取 quit 的值
  2. help 函数根据传入的参数 num 来创建 num 个子协程,并且使用 sync.WaitGroup 来控制
  3. 当主协程在 quit 通道中写入数据时,主动通知所有子协程退出
  4. help 中的另外一个协程读取到 quit 通道中的数据,便 close 掉 j 通道,触发所有的子协程读取 j 通道值的时候,得到的 ok 为 false,进而所有子协程退出
  5. wg.Wait() 等待所有子协程退出后,再在 quit 中写入数据
  6. 主协程此时从 quit 中读取到数据,则知道所有子协程全部退出,自己的主协程即刻退出
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 函数中阻塞住了,那么这个时候主协程岂不是一直无法退出???

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

  1. 设定一个定时器, 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 语言并发模式中的管道模式也是类似的效果,我们就可以用这个管道模式来过滤数据

例如我们可以设计这样一个程序,兄弟们可以动起手来写一写,评论区见哦:

  1. 整个程序总共使用 2 个通道
  2. help 函数中传输数据量 50 ,逻辑计算能够被 5 整除的数据写到第一个通道 ch1 中
  3. 另一个协程阻塞读取 ch1 中的内容,并将取出的数据乘以 3 ,将结果写入到 ch2 中
  4. 主协程就阻塞读取 ch2 的内容,读取到内容后,挨个打印出来

管道模式有两种模式,扇出模式 和 扇入模式,这个比较好理解

  1. 扇出模式:多种类型的数据从同一个通道 channel 中读取数据,直到通道关闭
  2. 扇入模式:输入的时候有多个通道channel,程序将所有的通道内数据汇聚,统一输入到另外一个通道channel A 里面,另外一个程序则从这个通道channel A 中读取数据,直到这个通道A关闭为止

超时模式和取消模式化

超时模式

上述例子中有专门说到如何去使用他,实际上我们还可以这样用:

select{
case <- time.Afer(time.Second * 2):
   fmt.Println("timeout programs exit. ")
case <- quit:
   fmt.Println(" 111 programs exit. ")
}

取消模式

则是使用了 GO 语言的 context 包中的提供了上下文机制,可以在协程 goroutine 之间传递 deadline,取消等信号

我们使用的时候例如可以这样:

  1. 使用 context.WithCancel 创建一个可以被取消的上下文,启动一个协程 在 3 秒后关闭上下文
  2. 使用 for 循环模拟处理业务,默认会走 select 的 default 分支
  3. 3 秒后 走到 select 的 ctx.Done(),则进入到了取消模式,程序退出
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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 学习Go语言并发编程,需要掌握以下几个方面: 1. 学习Goroutine:Goroutine是Go语言并发编程的核心,它是一种轻量级的线程,可以在程序中创建多个Goroutine并发执行,以实现高效的并发编程。需要学习如何创建和管理Goroutine。 2. 学习Channel:Channel是Goroutine之间进行通信的重要手段,可以用于数据传输和同步等操作。需要学习如何创建和使用Channel。 3. 学习Select语句:Select语句是Goroutine之间进行多路复用的重要语法,可以同时监听多个Channel,从而实现高效的并发处理。需要学习如何使用Select语句。 4. 学习Mutex和WaitGroup:Mutex和WaitGroup是Go语言中实现同步和互斥的重要机制,可以用于保护共享资源和协调Goroutine的执行。需要学习如何使用Mutex和WaitGroup。 5. 学习并发编程的设计模式并发编程的设计模式是一些常用的并发编程思想和模式,可以用于解决并发编程中的常见问题。需要学习如何应用并发编程的设计模式。 为了学习并发编程,可以参考一些优秀的Go语言并发编程书籍,比如《Go并发编程实战》、《Go语言并发与微服务实战》等。同时也可以参考一些优秀的开源项目,如etcd、Docker等,深入了解Go语言并发编程的应用场景和实现方式。 ### 回答2: 学习Go语言并发编程可以按照以下步骤进行。 1. 学习并理解Go语言并发模型:Go语言并发编程基于goroutine和channel。首先,需要了解goroutine的概念,它是Go语言中的一种轻量级线程。然后学习如何使用channel进行通信和同步。 2. 掌握goroutine的创建和管理:学习如何创建和管理goroutine,可以通过使用go关键字来创建一个新的goroutine,以便并发地执行任务。 3. 理解channel的使用:掌握channel的使用,了解如何创建、发送和接收数据。学习不同类型的channel以及它们在并发编程中的应用场景。 4. 学习互斥锁和读写锁:Go语言提供了互斥锁和读写锁来实现资源的安全访问。深入理解锁的概念和使用方法,学习如何避免并发访问导致的数据竞争。 5. 掌握并发编程的常见模式:学习并发编程中的常见模式,例如生产者-消费者模式、多路复用模式、线程池等。熟悉这些模式可以帮助我们更好地设计并发程序。 6. 阅读优秀的并发编程代码和文档:阅读优秀的并发编程代码可以提供实际的应用示例和启发。同时,阅读官方文档和相关书籍也是学习的重要途径。 7. 实践和调试:编写自己的并发代码,利用调试工具对程序进行调试,观察并发执行的过程和结果。通过实践来提高对并发编程的理解和应用能力。 总之,学习Go语言并发编程需要理解并发模型、掌握goroutine和channel的使用、了解锁的概念和使用方法,并通过实践来提高自己的技能。 ### 回答3: 学习Go语言并发编程可以通过以下几个步骤来进行。 首先,了解并发编程的概念和原则。并发编程是指同时进行多个任务,使用多个线程或协程来提高程序的效率和性能。了解并发编程的基本概念,如协程、锁、原子操作等,对学习Go语言并发编程非常重要。 其次,学习Go语言并发特性。Go语言内置了丰富的并发编程工具和特性,如goroutine、channel、select等。通过学习这些特性,可以更好地理解Go语言中的并发编程模型,并能够正确地使用它们。 然后,阅读相关的书籍和文章。有很多经典的书籍和文章涉及到Go语言并发编程,这些资源可以帮助你更深入地理解并发编程的原理和实践。值得推荐的书籍有《Go语言实战》和《Go并发编程实战》。 接着,进行实践和项目练习。通过编写一些小型的并发程序和项目,可以将理论知识应用到实践中,加深对并发编程的理解和掌握。可以尝试使用goroutine和channel来实现一些并发任务,如爬虫、并发请求等。 最后,参与社区和交流。加入Go语言的社区,如论坛、聊天群等,与其他开发者交流和分享经验。通过与他人的交流,可以学习到更多实践中的经验和技巧,不断提升自己的并发编程能力。 总之,学习Go语言并发编程需要系统地学习基本概念和原则,掌握语言特性,进行实践和项目练习,并积极参与社区和交流。只有不断实践和学习,才能在并发编程领域不断进步。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值