golang的协程置信大家都不生疏,在golang中的应用也很简略,只有加上一个关键字go即可,尽管说大家都晓得,然而真的在理论应用中又遇到这样那样的问题,坑其实还是挺多的。而网上很多文章和教程,要么就是讲的太简略,给你简略介绍一下协程和管道的应用,点到为止,要么就上来给你撸GPM模型,看的人一脸懵逼,所以我以理论应用过程中遇到的问题这个角度登程,可能会分多篇总结一下golang的协程相干的知识点,心愿对你有用,如果感觉还不错,记得点个赞,点个关注。

ps:如果你素来没有理解过golang的协程,倡议先本人搜一些材料简略的理解一下,还有并发并行那些根底概念之类的,本文都不会提及。

协程非常容易引发并发问题

咱们先看下列程序

func main() {    res := make(map[int]int)    for i := 0; i < 100; i++ {        go handleMap(res)    }    time.Sleep(time.Second * 1)}func handleMap(res map[int]int) {    for i := 0; i < 200; i++ {        res[i] = i * i    }}
  • 因为map类型作为参数是间接以援用的形式传递的,所以handleMap函数不须要返回值,间接操作参数res即可
  • handleMap的作用就是一直的给map赋值
  • 因为执行handleMap的时候是开启协程的,所以是多个程序并发的去对res(map类型写入),所以上述程序是会报错的,输入后果如下
  • 程序下方加上time.Sleep(time.Second * 1)的起因是因为主程序(main)执行结束退出,然而协程还没执行结束会被间接敞开。

    fatal error: concurrent map writesgoroutine 48 [running]:runtime.throw(0x100f814d1, 0x15)      /opt/homebrew/Cellar/go@1.16/1.16.13/libexec/src/runtime/panic.go:1117 +0x54 fp=0x14000145f50 sp=0x14000145f20 pc=0x100f16f34runtime.mapassign_fast64(0x100faeae0, 0x14000106180, 0x1f, 0x14000072200)      /opt/homebrew/Cellar/go@1.16/1.16.13/libexec/src/runtime/map_fast64.go:176 +0x2f8 fp=0x14000145f90 sp=0x14000145f50 pc=0x100ef7188main.handleMap(0x14000106180)      /Users/test/Sites/beikego/test/rountine.go:22 +0x44 fp=0x14000145fd0 sp=0x14000145f90 pc=0x100f7e644runtime.goexit()

    解决形式(1) 加锁

    如果有并发问题,咱们最容易想到的一个方法就是加锁

    func main() {  res := make(map[int]int)  for i := 0; i < 100000; i++ {      go handleMap(res)  }  time.Sleep(time.Second * 1)  lock.Lock()  //因为对map的读取的时候有可能还在写入,所以这里也须要加锁  for _, item := range res {      fmt.Println(item)  }  lock.Unlock()}func handleMap(res map[int]int) {  lock.Lock()  //每一个协程过去申请都先加锁  for i := 0; i < 2000; i++ {      res[i] = i * i  }  lock.Unlock()  //解决完map之后开释锁}

    下面过程我画了一张图,具体哪里为什么加锁都有阐明

  • 上述例子尽管开启了100000个协程,然而在每个协程解决map的时候加上了一个lock,处理完毕才开释,所以各个协程对map的操作是隔离开的
  • 在读取map的时候加锁的起因,是因为sleep 1s之后,有可能map还在写入,边读边写当然会有并发问题
    上述形式尽管解决了并发问题,然而也存在肯定的问题。次要是须要sleep,而且sleep多长时间没法确定
    所以这里引入咱们的解决形式2,管道

    解决形式(2)管道channel

channel实质就是一个数据结构,队列。既然是队列,当然有着先进先出的准则,而且是能保障线程平安的,多个gorountine拜访不须要加锁。

当然如果你还没有接触过管道,能够提前找些材料理解一下,上面是一个管道的简略示意图

管道在应用的过程中须要留神的问题

管道(channel)在应用的过程中有很多须要留神的点,我在这里列一下

应用管道之前必须make一下,而且指定长度

  var intChan chan int    intChan <- 1    fmt.Println(<-intChan)  //返回信息  fatal error: all goroutines are asleep - deadlock!  goroutine 1 [chan send (nil chan)]:

为什么须要make,后面文章曾经讲过,能够看看,
聊聊golang的make和new函数
指定长度也很好了解,管道的实质是队列,队列当然是须要指定长度的

管道写入的数据数如果超过管道长度,会报错

  intChan := make(chan int, 1)  //长度为1    intChan <- 1    intChan <- 2  //这里会报错    fmt.Println(<-intChan)  //返回后果  fatal error: all goroutines are asleep - deadlock!  goroutine 1 [chan send]:

读取空管道,会报错

intChan := make(chan int, 1)fmt.Println(<-intChan)  //此时管道外面还没有任何内容//返回后果fatal error: all goroutines are asleep - deadlock!goroutine 1 [chan receive]:

管道也反对interface,然而拿到构造体具体的属性的时候,须要断言

type Person struct {    Name string}func main(){    personChan := make(chan interface{}, 10)      personChan <- Person{Name: "小饭"}  //写入构造体类型      personChan <- 1  //写入int类型      personChan <- "test_string"  //写入string类型      fmt.Println(<-personChan, <-personChan, <-personChan)}  //返回后果  {小饭} 1 test_string

下面例子咱们能够看到,如果管道定义为interface类型,任何类型的数据都是能够写入并且失常取出的,然而咱们写入构造体类型之后,如果想取出构造体的具体属性,则须要断言

type Person struct {    Name string}func main() {    personChan := make(chan interface{}, 10)    personChan <- Person{Name: "小饭"}    person := <-personChan  //取出构造体之后,此时还不晓得是什么类型,所以没法间接取属性,因为定义的是interface    per := person.(Person)  //对取出后果进行断言    fmt.Println(per.Name)}//返回后果小饭

管道是能够循环的,然而循环之前必须敞开,敞开之后不可写入任何数据

  personChan := make(chan int, 10)    personChan <- 1    personChan <- 2    personChan <- 3    close(personChan) //敞开之后管道不能写入任何数据,否则就会报 panic: send on closed channel    for item := range personChan {  //在for range循环管道之前必须敞开管道,否则会报  fatal error: all goroutines are asleep - deadlock!        fmt.Println(item)    }
  • 其实为什么循环之前须要敞开管道,很好了解,因为for rang循环能够简略了解为一个死循环,当管道数据读取完了之后会持续读取,相似于读取一个空管道,当然会报错
  • 管道敞开之后不能写入更好了解,一个对象销毁了还能去赋值么?一样的情理

    切忌不要尝试用for(i:=0;i<len(chan):i++)的形式去循环

    这个很好了解,我就不必代码演示了,因为每次从管道中取一个数据,len(chan)是变动的,所以这么取数据必定是有问题的。换句话说也就是不要轻易用len(chan),坑很多

    协程和管道的综合应用

    咱们后面抛出的问题是,开启协程操作map会引发并发问题,当初咱们看看怎么用管道解决他

  • 留神这里用到了两个管道,管道chan map是用于map的读写用的exitChan是用于通知main函数能够退出用的
  • 首先开启一个writeMap的协程,把map数据都写入到管道(chan map)中,须要留神的是数据写完之后须要把协程敞开掉
  • 在开启一个readMap的协程,把管道中(chan map)数据一个一个的读出来.
  • 当readMap把数据全副读取实现中后,给main函数发送一个信号(也就是往exitChan中写一条数据)
  • main函数监听exitChan,收到数据间接退出即可。

    var chanMap chan map[int]intvar exitChan chan intfunc main() {  size := 50000  chanMap := make(chan map[int]int, size)    exitChan := make(chan int, 1)  go WriteMap(chanMap, size)  //开启写map协程  go ReadMap(chanMap, exitChan) //开启读map协程  for {      exit := <-exitChan  //监听exitChan 收到信号间接return即可      if exit != 0 {          return      }  }}//写map数据func WriteMap(chanMap chan map[int]int, size int) {  for i := 1; i <= size; i++ {      temp := make(map[int]int, 1)      temp[i] = i      chanMap <- temp      fmt.Println("写入数据:", temp)  }  close(chanMap)  //留神数据写完须要敞开管道}//读map数据func ReadMap(chanMap chan map[int]int, exitChan chan int) {  for {      val, ok := <-chanMap      if !ok {          break      }      fmt.Println("读取到:", val)  }  exitChan <- 1  //数据读取结束告诉main函数可退出}

    协程和管道到底能晋升多高的效率?

    咱们用协程的目标就是想进步程序的运行效率,管道能够简略了解为是帮助协程一起应用的,然而效率到底能晋升多少呢?咱们一起来看一看。

    判断素数

    大家都晓得,判断素数的复杂度是N²,比较慢,咱们先看一看传统的一个一个的去判断须要多长时间

    判断100000以内的数字哪些是素数
    func CheckPrime(num int) bool {  //判断一个数字是否是素数  res := true  for i := 2; i < num; i++ {      if num%i == 0 {          res = false      }  }  return res}func main(){t := time.Now()  size := 100000  for i := 0; i < size; i++ {      if CheckPrime(i) {          fmt.Println(i, "是素数")      }  }  elapsed := time.Since(t)  fmt.Println("app elapsed:", elapsed)  return}

    上述程序运行了3.33秒多,看来还是比较慢的

接下来咱们用协程和管道的形式看看,还是老规矩,咱们先看看流程图

  • 先把每个须要判断的数字写入initChan
  • 开启多个协程拉取initChan的数据一个一个的判断,这一步是程序速度放慢的要害,如果不是素数,不解决即可,如果是素数,就写入PrimeChan,判断完之后写入exitChan,告诉主程序即可
  • 主程序监听primeChan并输入,同时监听exitChan,收到信号退出即可
//初始化,把须要被判断的数字写入initChanfunc initChan(intChan chan int, size int) {    for i := 1; i <= size; i++ {        intChan <- i    }    close(intChan)}//读取initChan中的数据,一个一个的判断,如果是素数,就写入PrimeChan,并且写入exitChanfunc CheckPrimeChan(intChan, primeChan chan int, exitChan chan bool) {    for {        num, ok := <-intChan        if !ok {            break        }        if CheckPrime(num) {            primeChan <- num        }    }    exitChan <- true}func main() {    t := time.Now()     size := 100000    intChan := make(chan int, size)    primeChan := make(chan int, size)    exitChan := make(chan bool, 1)    go initChan(intChan, size) //初始化initChan    checkChannelNum := 8    for i := 0; i < checkChannelNum; i++ {  //开启8个协程同时拉取initChan的数据并判断是否是素数        go CheckPrimeChan(intChan, primeChan, exitChan)    }    go func() {        for i := 0; i < checkChannelNum; i++ {            <-exitChan        }        close(primeChan)    }()    for {        value, ok := <-primeChan        if !ok {            break        }        fmt.Println(value, "是素数")    }    elapsed := time.Since(t)    fmt.Println("app elapsed:", elapsed)}  //程序执行耗费工夫  848.455084m

上述程序执行工夫为848.455084ms,是传统的形式的工夫的四分之一,可见协程在进步运行效率这块的作用还是不言而喻的

本文由mdnice多平台公布