当前位置: 首页 > 科技观察

Golang的两大利器,协程和管道

时间:2023-03-12 12:54:00 科技观察

golang的协程大家都很熟悉了,在golang中的使用也很简单,加个关键字“go”就可以了。虽然大家都知道,但是在实际使用中,还是存在很多问题。但是网上很多文章和教程要么过于简单,简单的给大家介绍一下协程和管道的使用,所以我从“实际使用中遇到的问题”的角度出发,可能会在多篇文章中总结出golang的协程相关的知识点,希望对大家有用。ps:如果你没有了解过golang的协程,建议自己搜索一些资料简单了解一下,还有并发、并行的基本概念,本文不再赘述。协程很容易引起并发问题。我们先看下面的程序:funcmain(){res:=make(map[int]int)fori:=0;我<100;i++{去handleMap(res)}时间。睡眠(time.Second*1)}funchandleMap(resmap[int]int){fori:=0;我<200;i++{res[i]=i*i}}因为直接将map类型作为参数,通过引用传递,所以handleMap函数不需要返回值,直接操作参数res即可。handleMap的作用是不断给map赋值。因为在执行handleMap的时候开启了协程,多个程序并发写入res(map类型),所以上面的程序会报错,输出结果如下。之所以在程序下面加上time.Sleep(time.Second*1)是因为主程序(main)执行完就退出了,但是协程还没执行完就直接关闭了。致命错误:并发映射writesgoroutine48[running]:runtime.throw(0x100f814d1,0x15)/opt/homebrew/Cellar/go@1.16/1.16.13/libexec/src/runtime/panic.go:1117+0x54fp=0x14000145f50sp=0x14000145f20pc=0x100f16f34runtime.mapassign_fast64(0x100faeae0,0x14000106180,0x11f,0x140000722200)0x14000145f50pc=0x100ef7188main.handleMap(0x14000106180)/Users/test/Sites/beikego/test/rountine.go:22+0x44fp=0x14000145fd0sp=0x14000145f90pc=0x100f7e614)(如果有解决方法alock)(go)并发问题,我们能想到的最简单的方法之一就是加锁。funcmain(){res:=make(map[int]int)fori:=0;我<100000;i++{gohandleMap(res)}time.Sleep(time.Second*1)lock.Lock()//因为读的时候可能还是写了map,所以这里也需要加锁for_,item:=rangeres{fmt.Println(item)}lock.Unlock()}funchandleMap(resmap[int]int){lock.Lock()//每个协程首先请求加锁fori:=0;我<2000;i++{res[i]=i*i}lock.Unlock()//处理完map后释放锁}上面的过程我画了一张图,里面有解释为什么要加锁。上述程序的执行过程如上例所示。上面的例子虽然启用了10万个协程,但是在每个协程处理map的时候都会加一把锁,处理完才释放。因此,“图上各个协程的操作是隔离的”。读map时加锁的原因是因为sleep1s后,map可能还在写,当然读写也会有并发问题。上述方法虽然解决了并发问题,但是也存在一定的问题。主要是因为“需要睡眠,无法确定睡眠会持续多久”,所以这里介绍一下我们的方案2,管道。解决方案(2)管道通道通道的本质是一个“数据结构,队列”。既然是队列,当然有“先进先出”的原则,可以保证“线程安全”。多个goroutine访问不需要加锁。当然,如果你没有接触过管道,可以提前找一些资料了解一下。下面是一个简单的流水线示意图。管道使用过程中需要注意的问题管道(通道)使用过程中需要注意的点有很多,这里我一一列举。必须在使用pipeline之前make,并指定长度varintChanchanintintChan<-1fmt.Println(<-intChan)//返回信息fatalerror:allgoroutinesareasleep-deadlock!goroutine1[chansend(nilchan)]:为什么需要make?上篇文章说了,大家可以看看。下面说说golang的make和newfunctions。指定长度也很容易理解。“管道的本质是队列”。当然,队列需要指定长度。如果写入管道的数据数量超过了管道的长度,就会报错intChan:=make(chanint,1)//长度为1intChan<-1intChan<-2//会报错这里会报fmt.Println(<-intChan)//返回结果fatalerror:allgoroutinesareasleep-deadlock!goroutine1[chansend]:读取空管道,会报错intChan:=make(chanint,1)fmt.Println(<-intChan)//此时pipeline里面什么都没有//返回结果fatalerror:所有的goroutines都睡着了——僵局!goroutine1[chanreceive]:pipeline也支持interface,但是在获取结构体的具体属性时,需要asserttypePersonstruct{Namestring}funcmain(){personChan:=make(chaninterface{},10)personChan<-Person{Name:"Xiaofan"}//写结构体类型personChan<-1//写int类型personChan<-"test_string"//写字符串类型fmt.Println(<-personChan,<-personChan,<-personChan)}//Returnresult{Xiaofan}1test_string在上面的例子中我们可以看到,如果管道定义为接口类型,任何类型的数据都可以正常写入和获取,但是在我们写入“结构类型”之后,如果我们想检索结构的“特定属性”,我们需要断言。typePersonstruct{Namestring}funcmain(){personChan:=make(chaninterface{},10)personChan<-Person{Name:"Xiaofan"}person:=<-personChan//取出结构体后,这个还是不知道是什么类型,所以不能直接获取属性,因为定义是interfaceper:=person.(Person)//断言提取的结果fmt.Println(per.Name)}//返回结果Xiaofanpipeline可以循环,但是循环前必须关闭,关闭后不能写入数据personChan:=make(chanint,10)personChan<-1personChan<-2personChan<-3close(personChan)//关闭后的管道不能写入数据,否则会报panic:sendonclosedchannelforitem:=rangepersonChan{//管道必须在forrange循环管道之前关闭,否则a会报致命错误:allgoroutinesaresleep-deadlock!英尺。Println(item)}其实很容易理解为什么循环前需要先关闭管道,因为forrang循环可以简单理解为死循环。当管道数据被读取时,会继续读取,类似于读取一个空的管道,当然会报错。最好理解为管道关闭后不能写入。对象销毁后还能赋值吗?同样的道理。不要尝试使用for(i:=0;i