golang的协程相信大家都不陌生,在golang中的使用也很简单,只要加上一个关键字go即可,虽然说大家都知道,但是真的在实际使用中又遇到这样那样的问题,坑其实还是挺多的。而网上很多文章和教程,要么就是讲的太简单,给你简单介绍一下协程和管道的使用,点到为止,要么就上来给你撸GPM模型,看的人一脸懵逼,所以我以实际使用过程中遇到的问题这个角度出发,可能会分多篇总结一下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 }}复制代码
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()复制代码
如果有并发问题,我们最容易想到的一个办法就是加锁
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之后释放锁}复制代码
上面过程我画了一张图,具体哪里为什么加锁都有说明
channel本质就是一个数据结构,队列。既然是队列,当然有着先进先出的原则,而且是能保证线程安全的,多个gorountine访问不需要加锁。
当然如果你还没有接触过管道,可以提前找些资料了解一下,下面是一个管道的简单示意图
管道(channel)在使用的过程中有很多需要注意的点,我在这里列一下
var intChan chan int intChan <- 1 fmt.Println(<-intChan) //返回信息 fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan send (nil chan)]:复制代码
为什么需要make,指定长度也很好理解,管道的本质是队列,队列当然是需要指定长度的
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]:复制代码
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) }复制代码
这个很好理解,我就不用代码演示了,因为每次从管道中取一个数据,len(chan)是变化的,所以这么取数据肯定是有问题的。换句话说也就是不要随便用len(chan),坑很多
我们前面抛出的问题是,开启协程操作map会引发并发问题,现在我们看看怎么用管道解决他
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²,比较慢,咱们先看一看传统的一个一个的去判断需要多长时间
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秒多,看来还是比较慢的
接下来我们用协程和管道的方式看看,还是老规矩,我们先看看流程图
//初始化,把需要被判断的数字写入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,是传统的方式的时间的四分之一,可见协程在提高运行效率这块的作用还是显而易见的