关于go并发编程的总结

关于go并发编程的总结

1.使用sync进行并发:

package main
  
  import (
      "fmt"
      "sync"
  )
  
  func main() {
  
      testSlice := []string{"test1", "test2", "test3"}
      wg := sync.WaitGroup{}
      for _, t := range testSlice {
          wg.Add(1)
          go printSlice(t, &wg)
      }   
      wg.Wait()
  }
  
  func printSlice(s string, wg *sync.WaitGroup) {
      defer wg.Done()
      fmt.Printf("this is %+v\n", s)
  }          

sync包实现并发的核心就是waitgroup,初始化一个WaitGroup类型的指针,需要并发时,则使用Add方法,添加计数,并在需要进行并发的函数中将其作为参数传入,当这个函数操作完成后,调用Done方法减掉计数;在主协程中,Wait方法会一直监听计数器的数量,当计数器为0时,说明所有的并发函数都完成了,这时主协程就可以退出了;个人感觉这是最简单的实现并发的方式,在需要并发处理一个集合内的所有数据时尤其好用;

2.使用管道进行并发

管道是go原生支持的数据类型,使用它也能达到并发的效果
通常的思路是,在主协程取管道中的数据,这时管道会阻塞,在发送协程中向管道里塞数据,塞完数据后关闭掉管道;当主协程取不到数据,管道也关闭后,任务就完成了

package main

  import "fmt"

  var channel = make(chan int, 10)

  func main() {
      go func() {
          for i := 0; i < 10; i++ {
              channel <- i
          }
          close(channel) //放完数据后一定要关闭chan,否则会死锁;
      }()

      for v := range channel {
          fmt.Println(v)
      } //在主协程从chan里取数据,因为取不到会一直阻塞,这样main routine就不会退出;
      //如果另起一个协程取数据,在另一个协程里阻塞,但主协程并未阻塞,取数据协程还没取到,主协程就退出了;
  }

上面的例子其实并没有体现出并发执行,因为十个数按次序塞进管道中,主协程按次序从管道里取出了数据,还是一个串行的过程

进阶版

package main

  import (
      "fmt"
  )

  var channel = make(chan int)
  var result = make(chan int)

  func main() {
      go func() {
          for i := 0; i < 100; i++ {
              channel <- i
          }
          close(channel)
      }()

      ct := 0
      for c := range channel {
          go func(i int) {
              result <- i + i
          }(c)

      }

      for v := range result {
          ct++
          fmt.Println(v)
          if ct == 100 {
              break
          }
      }
  }

把数据按次序投入到管道中后,遍历管道,每取出一个数据,则开启一个新的协程来做计算工作(i+i),然后将结果放到result队列中,最后在主协程取出result管道的值;由于需要关闭result管道,但是关闭的位置不好确定,目前暂时按计算的数据量来决定跳出循环的时机,大家也可讨论下什么时候应该关闭result管道;

注意,关闭管道与监听管道取值需要是两个不同的协程,若两个操作都在一个协程,要么监听了一个已经关闭的协程,要么监听了一个没有被关闭的协程,都会产生异常

在这里,管道的容量是0,即每次往管道塞数据需要等里面的数据被消费后才能继续塞;有容量的协程则是可以塞进数量为容量数的数据,之后的数据需要阻塞,直到有空余;

3. select关键字

先看代码

    start := time.Now()
    c := make(chan interface{})
    ch1 := make(chan int)
        ch2 := make(chan int)

    go func() {

        time.Sleep(4*time.Second)
        close(c)
    }()

    go func() {

        time.Sleep(3*time.Second)
        ch1 <- 3
    }()

      go func() {

        time.Sleep(3*time.Second)
        ch2 <- 5
    }()

    fmt.Println("Blocking on read...")
    select {
    case <- c:
        fmt.Printf("Unblocked %v later.\n", time.Since(start))
    case <- ch1:
        fmt.Printf("ch1 case...")
      case <- ch2:
        fmt.Printf("ch2 case...")
    default:
        fmt.Printf("default go...")
    }

运行上述代码,由于当前时间还未到3s。所以,目前程序会走default。

若注释掉default,ch1,ch2分支会随机挑选一个执行,若将ch1 ch2的sleep时间改为10秒,则会执行c分支;

  • 如果有一个或多个关于管道操作可以完成,则Go运行时系统会随机的选择一个执行,否则的话,如果有default分支,则执行default分支语句,如果连default都没有,则select语句会一直阻塞,直到至少有一个管道操作可以进行.
  • 需要有真实的goroutine存在,若所有对管道操作的的子goroutine都已退出,select{}会报panic

部分参考:https://www.jianshu.com/p/2a1…

    原文作者:byte
    原文地址: https://segmentfault.com/a/1190000020107790
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞