您好,欢迎访问一九零五行业门户网

go语言怎么实现并发控制

go语言实现并发控制的方法:1、waitgroup,多个goroutine的任务处理存在依赖或拼接关系;2、channel,可以主动取消goroutine;多groutine中数据传递;代替waitgroup的工作,满足context的功能;3、context,多层级groutine之间的信号传播,包括元数据传播,取消信号传播、超时控制等。
本文的操作环境:windows10系统、go1.20版本、dell g3电脑。
golang中通过go关键字就可开启一个goroutine,因此,在go中可以轻松写出并发代码。但是,如何对这些并发执行的groutines有效地控制?
提到并发控制,很多人可能最先想到的是锁。golang中同样提供了锁的相关机制,包括互斥锁sync.mutex,和读写锁sync.rwmutex。除了锁,还有原子操作sync/atomic等。但是,这些机制关注的重点是goroutines的并发数据安全性。而本文想讨论的是goroutine的并发行为控制。
在goroutine并发行为控制中,有三种常见的方式,分别是waitgroup、channel和context。
waitgroupwaitgroup位于sync包下,它的使用方法如下。
func main() {  var wg sync.waitgroup  wg.add(2) //添加需要完成的工作量2  go func() {    wg.done() //完成工作量1    fmt.println(goroutine 1 完成工作!)  }()  go func() {    wg.done() //完成工作量1    fmt.println(goroutine 2 完成工作!)  }()  wg.wait() //等待工作量2均完成  fmt.println(所有的goroutine均已完成工作!)}输出://goroutine 2 完成工作!//goroutine 1 完成工作!//所有的goroutine均已完成工作!
waitgroup这种并发控制方式尤其适用于:某任务需要多 goroutine 协同工作,每个 goroutine 只能做该任务的一部分,只有全部的 goroutine 都完成,任务才算是完成。因此,waitgroup同名字的含义一样,是一种等待的方式。
但是,在实际的业务中,有这么一种场景:当满足某个要求时,需主动的通知某一个 goroutine 结束。比如我们开启一个后台监控goroutine,当不再需要监控时,就应该通知这个监控 goroutine 结束,不然它会一直空转,造成泄漏。
channel对于上述场景,waitgroup无能为力。那能想到的最简单的方法:定义一个全局变量,在其它地方通过修改这个变量进行通知,后台 goroutine 会不停的检查这个变量,如果发现变量发生了变化,即自行关闭,但是这个方法未免有些笨拙。这种情况,channel+select可派上用场。
func main() {  exit := make(chan bool)  go func() {    for {      select {      case <-exit:        fmt.println(退出监控)        return      default:        fmt.println(监控中)        time.sleep(2 * time.second)      }    }  }()  time.sleep(5 * time.second)  fmt.println(通知监控退出)  exit <- true  //防止main goroutine过早退出  time.sleep(5 * time.second)}输出://监控中//监控中//监控中//通知监控退出//退出监控
这种 channel+select 的组合,是比较优雅的通知goroutine 结束的方式。
但是,该方案同样存在局限性。试想,如果有多个 goroutine 都需要控制结束怎么办?如果这些 goroutine 又衍生了其它更多的goroutine 呢?当然我们可以定义很多 channel 来解决这个问题,但是 goroutine 的关系链导致这种场景的复杂性。
context以上场景常见于cs架构模型下。在go中,常常为每个client开启单独的goroutine(a)来处理它的一系列request,并且往往单个a中也会请求其他服务(启动另一个goroutine b),b也可能会请求另外的goroutine c,c再将request发送给例如databse的server。设想,当client断开连接,那么与之相关联的a、b、c均需要立即退出,系统才可回收a、b、c所占用的资源。退出a简单,但是,如何通知b、c也退出呢?
这个时候,context就出场了。
func a(ctx context.context, name string)  {  go b(ctx ,name) //a调用了b  for {    select {    case <-ctx.done():      fmt.println(name, a退出)      return    default:      fmt.println(name, a do something)      time.sleep(2 * time.second)    }  }}func b(ctx context.context, name string)  {  for {    select {    case <-ctx.done():      fmt.println(name, b退出)      return    default:      fmt.println(name, b do something)      time.sleep(2 * time.second)    }  }}func main() {  ctx, cancel := context.withcancel(context.background())  go a(ctx, 【请求1】) //模拟client来了1个连接请求  time.sleep(3 * time.second)  fmt.println(client断开连接,通知对应处理client请求的a,b退出)  cancel() //假设满足某条件client断开了连接,那么就传播取消信号,ctx.done()中得到取消信号  time.sleep(3 * time.second)}输出://【请求1】 a do something//【请求1】 b do something//【请求1】 a do something//【请求1】 b do something//client断开连接,通知对应处理client请求的a,b退出//【请求1】 b退出//【请求1】 a退出
示例中模拟了客户端来了连接请求,相应开启goroutine a进行处理,a同时开启了b处理,a和b都使用了 context 进行跟踪,当我们使用 cancel 函数通知取消时,这 2个 goroutine 都会被结束。
这就是 context 的控制能力,它就像一个控制器一样,按下开关后,所有基于这个 context 或者衍生的子 context 都会收到通知,这时就可以进行清理操作了,最终释放 goroutine,这就优雅的解决了 goroutine 启动后不可控的问题。
关于context的详细用法,不在本文讨论范围之内。后续会出专门对context包的讲解文章,敬请关注。
总结本文列举了三种golang中并发行为控制模式。模式之间没有好坏之分,只在于不同的场景用恰当的方案。实际项目中,往往多种方式混合使用。
waitgroup:多个goroutine的任务处理存在依赖或拼接关系。channel+select:可以主动取消goroutine;多groutine中数据传递;channel可以代替waitgroup的工作,但会增加代码逻辑复杂性;多channel可以满足context的功能,同样,也会让代码逻辑变得复杂。context:多层级groutine之间的信号传播(包括元数据传播,取消信号传播、超时控制等)。golang中通过go关键字就可开启一个goroutine,因此,在go中可以轻松写出并发代码。但是,如何对这些并发执行的groutines有效地控制?
提到并发控制,很多人可能最先想到的是锁。golang中同样提供了锁的相关机制,包括互斥锁sync.mutex,和读写锁sync.rwmutex。除了锁,还有原子操作sync/atomic等。但是,这些机制关注的重点是goroutines的并发数据安全性。而本文想讨论的是goroutine的并发行为控制。
在goroutine并发行为控制中,有三种常见的方式,分别是waitgroup、channel和context。
以上就是go语言怎么实现并发控制的详细内容。
其它类似信息

推荐信息