golang并发模型

**题目来源:**米哈游

参考文章

golang控制并发有三种经典的方式

  1. 一种是通过channel通知实现并发控制
  2. 一种是WaitGroup
  3. 另外一种就是Context

1、使用最基本通过channel通知实现并发控制

无缓冲通道:
无缓冲的通道指的是通道的大小为0,也就是说,这种类型的通道在接收前没有能力保存任何值,它要求发送 goroutine 和接收 goroutine 同时准备好,才可以完成发送和接收操作。
从上面无缓冲的通道定义来看,发送 goroutine 和接收 gouroutine 必须是同步的,同时准备后,如果没有同时准备好的话,先执行的操作就会阻塞等待,直到另一个相对应的操作准备好为止。这种无缓冲的通道我们也称之为同步通道。
例子:

  1. func main() {
  2. ch := make(chan struct{})
  3. go func() {
  4. ch <- struct{}{}
  5. }()
  6. fmt.Println(<-ch)
  7. }

解释
当主 goroutine 运行到 <-ch 接受 channel 的值的时候,如果该 channel 中没有数据,就会一直阻塞等待,直到有值。 这样就可以简单实现并发控制。

2.通过sync包中的WaitGroup实现并发控制

在 sync 包中,提供了 WaitGroup ,它会等待它收集的所有 goroutine 任务全部完成,在主 goroutine 中 Add(delta int) 索要等待goroutine 的数量。
在每一个 goroutine 完成后 Done() 表示这一个goroutine 已经完成,当所有的 goroutine 都完成后,在主 goroutine 中 WaitGroup 返回返回。

例子:

  1. fun main(){
  2. var wg sync.WaitGroup
  3. var urls = []string{
  4. "http://www.golang.org/",
  5. "http://www.sensetime.com/",
  6. "http://www.baidu.com/",
  7. }
  8. for _, url := range urls {
  9. wg.Add(1)
  10. go func(url string) {
  11. defer wg.Done()
  12. http.Get(url)
  13. }(url)
  14. }
  15. wg.Wait()
  16. }

3. 在Go 1.7 以后引进的强大的Context上下文,实现并发控制

在一些简单场景下使用 channel 和 WaitGroup 已经足够了,但是当面临一些复杂多变的网络并发场景下 channel 和 WaitGroup 显得有些力不从心了。 比如一个网络请求 Request,每个 Request 都需要开启一个 goroutine 做一些事情,这些 goroutine 又可能会开启其他的 goroutine,比如数据库和RPC服务。
所以我们需要一种可以跟踪 goroutine 的方案,才可以达到控制他们的目的,这就是Go语言为我们提供的 Context,称之为上下文非常贴切,它就是goroutine 的上下文。
它是包括一个程序的运行环境、现场和快照等。每个程序要运行时,都需要知道当前程序的运行状态,通常Go 将这些封装在一个 Context 里,再将它传给要执行的 goroutine 。
context 包主要是用来处理多个 goroutine 之间共享数据,及多个 goroutine 的管理。
context包方法:
Done() 返回一个只能接受数据的channel类型,当该context关闭或者超时时间到了的时候,该channel就会有一个取消信号
Err() 在Done() 之后,返回context 取消的原因。
Deadline() 设置该context cancel的时间点
Value() 方法允许 Context 对象携带request作用域的数据,该数据必须是线程安全的

例子:

  1. func childFunc(cont context.Context, num *int) {
  2. ctx, _ := context.WithCancel(cont)
  3. for {
  4. select {
  5. case <-ctx.Done():
  6. fmt.Println("child Done : ", ctx.Err())
  7. return
  8. }
  9. }
  10. }
  11. func main() {
  12. gen := func(ctx context.Context) <-chan int {
  13. dst := make(chan int)
  14. n := 1
  15. go func() {
  16. for {
  17. select {
  18. case <-ctx.Done():
  19. fmt.Println("parent Done : ", ctx.Err())
  20. return // returning not to leak the goroutine
  21. case dst <- n:
  22. n++
  23. go childFunc(ctx, &n)
  24. }
  25. }
  26. }()
  27. return dst
  28. }
  29. ctx, cancel := context.WithCancel(context.Background())
  30. for n := range gen(ctx) {
  31. fmt.Println(n)
  32. if n >= 5 {
  33. break
  34. }
  35. }
  36. cancel()
  37. time.Sleep(5 * time.Second)
  38. }

在上面的例子中,主要描述的是通过一个channel实现一个为循环次数为5的循环,在每一个循环中产生一个goroutine,每一个goroutine中都传入context,在每个goroutine中通过传入ctx创建一个子Context,并且通过select一直监控该Context的运行情况,当在父Context退出的时候,代码中并没有明显调用子Context的Cancel函数,但是分析结果,子Context还是被正确合理的关闭了,这是因为,所有基于这个Context或者衍生的子Context都会收到通知,这时就可以进行清理操作了,最终释放goroutine,这就优雅的解决了goroutine启动后不可控的问题。