我们上一章了解了什么是协程,我们在开始前先做一个练习


练习目标


我们现在要计算1-200的各个数的阶乘,并且把每个数的阶乘放入到map中

最后显示出来,要求使用goroutine完成


package main

import (
"fmt"
)


var (
myMap = make(map[int]int,10) //声明一个map用来存放阶乘
)


func test(n int) { //编写函数test 计算阶乘并写入到map中
res := 1
for i := 1; i <= n; i++{
res *= i
}
myMap[n] = res
}

func main(){
for i := 1; i <= 200; i++ {
go test(i) //循环开启200个协程去写入到map
}


for i, v := range myMap {
fmt.Printf("map[%d]=%d\n",i,v) //输出结果
}

}

返回

fatal error: concurrent map writes


不知道大家有没有考虑过一个问题,我这里开200个协程去调用test函数,每个协程都是有

写操作,我怎么知道这些协程谁先去写入到map呢?这就出现了共享变量并发导致竞争关系


 Golang学习(三十二) 互斥锁与管道_数据


竞争关系目前有两种解决方法 添加互斥锁和设置管道


一、互斥锁


互斥锁就是给变量进行加锁和解锁的操作,当我们要写入到变量数据时,

先去找这个锁,如果前面有数据在写入就加锁,如果前面写入完了就解锁

 其他协程来找锁如果发现锁定了,就进入队列等待


 Golang学习(三十二) 互斥锁与管道_数据_02


 在go中互斥锁由sync包提供,  lock(加锁)  unlock(解锁)


Golang学习(三十二) 互斥锁与管道_学习_03


 官方这里也说了,处理竞争关系channel管道更好一些,不过这里还是演示一下互斥锁


案例

package main

import (
"fmt"
"sync"
)


var (
myMap = make(map[int]int,10)

lock sync.Mutex //声明一个全局变量的互斥锁 变量名是lock
)





func test(n int) {
res := 1
for i := 1; i <= n; i++{
res *= i
}

lock.Lock() //加锁操作 我们之前出现的问题就是没有写入顺序
//这里在写入之前加个锁,当Unlock()解锁后下一个协程才会写入
//当第一个协程在写入时,其他协程会卡在加锁的位置
myMap[n] = res

lock.Unlock() //写入完成后解锁
}

func main(){
for i := 1; i <= 200; i++ {
go test(i)
}

lock.Lock() //在我们输出变量myMap时也需要加锁
//因为我们的程序从设计上可以知道,协程很快就执行完成了
//但主线程并不知道,因此底层可能仍然出现资源争夺
//还是会爆竞争错误,我们给他加锁即可恢复

for i, v := range myMap {
fmt.Printf("map[%d]=%d\n",i,v)
}
lock.Unlock() //解锁
}


我们返回的结果有很多都是0,这是因为我们map设置的类型是int,它的最大值是2的63次方-1,当数值大于这个数时就存不进去了,它输出的就是默认值0了,负数也是没有意义的


package main

import (
"fmt"
"sync"
)


var (
myMap = make(map[int]int,10)
lock sync.Mutex
)





func test(n int) {

res := 1
for i := 1; i <= n; i++{
res *= i
}


lock.Lock()
myMap[n] = res
lock.Unlock()
}

func main(){
for i := 1; i <= 20; i++ { //修改大小
go test(i)
}

lock.Lock()
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n",i,v)
}
lock.Unlock()
}

返回

map[3]=6
map[4]=24
map[6]=720
map[13]=6227020800
map[10]=3628800
map[14]=87178291200
map[16]=20922789888000
map[15]=1307674368000
map[5]=120
map[1]=1
map[2]=2
map[9]=362880
map[18]=6402373705728000
map[11]=39916800
map[12]=479001600
map[17]=355687428096000
map[19]=121645100408832000
map[8]=40320
map[7]=5040


你会发现,它每次返回的值都不同,这是因为它上面的协程还没跑完

主线程就已经输出了,我们给它添加一个延时时间,让主线程等一等协程


package main

import (
"fmt"
"sync"
"time"
)


var (
myMap = make(map[int]int,10)
lock sync.Mutex
)


func test(n int) {
res := 1
for i := 1; i <= n; i++{
res *= i
}

lock.Lock()
myMap[n] = res
lock.Unlock()
}

func main(){
for i := 1; i <= 20; i++ {
go test(i)
}

time.Sleep(time.Second * 5) //延时5秒
lock.Lock()
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n",i,v)
}
lock.Unlock()
}


让他们计算完后,主线程在输出,但是这样的解决方法很low

因为我们不清楚协程具体多久才能跑完,设置的时间也不确定,我们还是用channel吧


二、channel 管道


面使用全局遍历加锁同步来解决goroutine的通讯,但不完美,还保留以下问题


1、主线程在等待所有的"goroutine"全部完成的时间很难确定,完美这里设置5秒仅仅是估算
//如果不设置休眠时间,协程还没写完数据就结束了

2、如果主线程休眠时间长了,会加长等待时间,如果等待时间短了
可能还有goroutine处于工作状态这时也会随主线程的退出而销毁

3、通过全局变量加锁同步来实现通讯,也并不利于多个协程对全局变量的读写操作

1、channel基本介绍

1、 channel本质就是一个数据结构--队列

2、 数据是先进先出 //FIFO: first in firstout

3、 线程安全,多"goroutine"访问时,不需要加锁,就是说channel本身就是线程安全的

4、 channel时有类型的,一个string的channel只能存放string类型数据
//如果想要使用不同类型,那么可以声明为空接口interface{},不过不推荐使用

队列示意图

 Golang学习(三十二) 互斥锁与管道_i++_04

2、管道声明格式

var 变量名 chan 数据类型

声明案例

var intChan chan int              //intChan用于存放int数据)

var mapChan chan map{int}string //mapChan用于存放map[int]string类型

var perChan chan Person //结构体

var perChan2 chan *Person //结构体指针

说明

1、  channel 是引用类型

2、 channel必须初始化才能写入数据,即make后才能使用

3、 管道是有类型的,intChan只能写入整数int

3、channel快速入门

package main

import (
"fmt"
)

func main(){
var intChan chan int //声明一个int类型的管道

intChan = make(chan int,3) //分配内存 空间大小为3

fmt.Printf("intChan的值=%v intChan本身的地址%p",intChan,&intChan)
}

 返回

intChan的值=0xc042074080 intChan本身的地址0xc042004028


0xc042074080  是管道内  存放的值,说明它指向了这个管道的内存地址
0xc042004028  是intChan这个变量的内存地址


Golang学习(三十二) 互斥锁与管道_数据_05

4、向管道写入数据

package main

import (
"fmt"
)

func main(){
var intChan chan int
intChan = make(chan int,3)

//fmt.Printf("intChan的值=%v intChan本身的地址%p\n",intChan,&intChan)

intChan<- 10 //"<-" 写入数据
num := 211
intChan<- num

//注意,当我们给管道写入数据时,不能超过其最大容量
//否则会报 fatal error: all goroutines are asleep - deadlock! 死锁

//看看管道的长度和cap()容量
fmt.Printf("channel len=%v cap=%v \n",len(intChan),cap(intChan))
}

返回

channel len=2 cap=3


len为2 是因为我们写入了两个数据,cap 为3 是因为我们make的时候设置的空间大小


5、从管道中读取数据

package main

import (
"fmt"
)

func main(){
var intChan chan int
intChan = make(chan int,3)

//fmt.Printf("intChan的值=%v intChan本身的地址%p\n",intChan,&intChan)
intChan<- 10
num := 211
intChan<- num
fmt.Printf("channel len=%v cap=%v \n",len(intChan),cap(intChan))




num2 := <-intChan //从管道读取数据 "<-intChan"
//先进先出原则,会读取到10
//当没有数据后,再去取数据也会发生死锁
fmt.Println(num2)
fmt.Printf("channel len=%v cap=%v \n",len(intChan),cap(intChan))

}


注意读取管道时"<-"的位置,如果变量在前则为写入数据,变量在后是读取数据


三、channel使用的注意事项

1、channel中只能存放指定的数据类型

2、channel的数据放满后,就不能再放入了

3、如果从channel取出数据后,可以继续放入
//并且可以使用<-intChan 不用变量接收,这样就会丢弃数据

4、 在没有使用协程的情况下,如果channel数据取完了,再取就会爆dead lock

四、channel管道读取案例

案例1 存取map

package main

import "fmt"

func main(){

var mapChan chan map[string]string
mapChan = make(chan map[string]string,10)
m1 := make(map[string]string,20)
m1["city1"] = "北京"
m1["city2"] = "天津"

m2 := make(map[string]string,20)
m2["hero1"] = "宋江"
m2["hero2"] = "武松"

mapChan <- m1
mapChan <- m2

fmt.Println(<-mapChan)
fmt.Println(<-mapChan)
}

案例2  存取结构体

package main

import (
"fmt"
)



type Cat struct {
Name string
Age int
}
func main(){

var catChan chan Cat
catChan = make(chan Cat,10)

cat1 := Cat{Name:"tom",Age:18,}
cat2 := Cat{Name:"tom~",Age:118,}

catChan <- cat1
catChan <- cat2

//取出
cat11 := <-catChan
cat22 := <-catChan

fmt.Println(cat11,cat22)

}

案例3 存储指针数据

package main

import (
"fmt"
)



type Cat struct {
Name string
Age int
}
func main(){

var catChan chan *Cat
catChan = make(chan *Cat,10)

cat1 := Cat{Name:"tom",Age:18,}
cat2 := Cat{Name:"tom~",Age:118,}

catChan <- &cat1 //写入时改为地址
catChan <- &cat2

//取出
cat11 := <-catChan
cat22 := <-catChan

fmt.Println(cat11,cat22)

}

案例4  混合类型存储


混合数据类型需要设置chan管道类型为空接口类型,但不推荐这么使用


package main

import (
"fmt"
)



type Cat struct {
Name string
Age int
}
func main(){

var allChan chan interface{}
allChan = make(chan interface{},10)

cat1 := Cat{Name:"tom",Age:18,}
cat2 := Cat{Name:"tom~",Age:180,}
allChan <- cat1
allChan <- cat2
allChan <- 10
allChan <- "jack"

//取值
cat11 := <-allChan
cat22 := <-allChan
v1 := <-allChan
v2 := <-allChan

fmt.Println(cat11,cat22,v1,v2)
}

案例5  混合类型存储错误案例

package main

import (
"fmt"
)

type Cat struct{
Name string
Age int
}
func main(){
var allChan chan interface{}
allChan = make(chan interface{},10)

cat1 := Cat{Name:"tom",Age:18}
cat2 := Cat{Name:"tom~",Age:180}

allChan <- cat1
allChan <- cat2
allChan <- 10
allChan <- "jack"
//取出
cat11 := <- allChan
fmt.Println(cat11.Name)
}


cat11 := <- allChan  这里我们从管道读取出来的数据其实是空接口类型的

我们无法通过空接口来调用结构体下的方法,需要将这个空接口转换回结构体

这里就眼熟了,直接使用我们前面的类型断言进行转换


package main

import (
"fmt"
)

type Cat struct{
Name string
Age int
}
func main(){
var allChan chan interface{}
allChan = make(chan interface{},10)

cat1 := Cat{Name:"tom",Age:18,}
cat2 := Cat{Name:"tom~",Age:180,}

allChan <- cat1
allChan <- cat2
allChan <- 10
allChan <- "jack"
//取出
cat11 := <- allChan

fmt.Println(cat11.(Cat).Name) //添加类型断言
}

五、关闭管道


使用内置函数close可以关闭channel,当channel关闭后

就不能再向channel"写数据了,但是仍然可以从该channel读取数据

来自go语言内置函数 builtin包 下的close方法


 Golang学习(三十二) 互斥锁与管道_学习_06

案例

package main

import "fmt"

func main(){
intChan := make(chan int,3)
intChan<- 100
intChan<- 200

close(intChan) //close是用于关闭管道
//如果关闭管道后再写入,会爆panic: send on closed channel
fmt.Println("okok")


n1 := <-intChan //允许读取
fmt.Println("n1=",n1)
}

六、遍历管道


channel支持for-range的方式进行遍历,请注意两个细节


1、在遍历时,如果channel没有关闭,则会出现deadlock的错误

2、在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历


 怎么确认你协程完成工作了呢?,我们通过关闭管道,来确认协程完成了工作


案例1 为什么不用for循环

package main

import "fmt"

func main(){
var intChan chan int
intChan = make(chan int,100)

for i := 0; i < cap(intChan);i++{
intChan <- i+1
}

for i := 0; i < len(intChan); i ++{
k := <-intChan
fmt.Println(k)
}
}


发现值到50就停了, 因为管道,长度会变化,而单独的for循环是无法确认的


案例2 for-range遍历

package main

import "fmt"

func main(){
var intChan chan int
intChan = make(chan int,100)
for i := 0; i < cap(intChan);i++{
intChan <- i+1
}

//使用for-range处理
for v := range intChan{
fmt.Println(v)
}
}

返回

1-100   
fatal error: all goroutines are asleep - deadlock!


我们可以看到,它取出了所有的数据之后报错了,这是因为我们没有关闭管道


 添加管道关闭

package main

import "fmt"

func main(){
var intChan chan int
intChan = make(chan int,100)
for i := 0; i < cap(intChan);i++{
intChan <- i+1
}

close(intChan) //添加管道关闭
for v := range intChan{
fmt.Println(v)
}
}


这样即使没有读取到数据,也不会爆deadlock!了

所以说如果要for-range遍历管道,那么必须先关闭管道


七、channel使用细节和注意事项

1、channel可以声明为"只读"或者"只写"的性质

func main(){

var chan1 chan int //在默认情况下,管道是双向的,可读可写


var chan2 chan<- int //声明为只写管道 (<-)
chan2 = make(chan int,3)
chan2<- 20
fmt.Println("chan2=",chan2)



var chan3 <-chan int //声明为只读管道 只读不写
num2 := <-chan3
fmt.Println("num2",num2)

}


只读只写在什么情况下会用呢?


package main

import (
"fmt"
)




//ch chan<- int 这样ch就只能写操作了
func send(ch chan<- int,exitChan chan struct{}){
for i := 0; i < 10; i++{
ch <- i
}
close(ch)
var a struct{}
exitChan<-a
}

//ch <-chan int 这样ch就只能只读了
func recv(ch <-chan int, exitChan chan struct{}){
for {
v,ok := <-ch
if !ok{
break
}
fmt.Println(v)
}
var a struct{}
exitChan<-a
}


func main(){
var ch chan int //创建了一个双向通道
ch = make(chan int,10)
exitChan := make(chan struct{},2)

go send(ch, exitChan)
go recv(ch, exitChan)

var total = 0
for _ = range exitChan{
total++
if total == 2{
break
}
}
fmt.Println("结束")

}


 在函数传参的时候设置接收的模式,这样有效的防止我们误操作


2、使用select 可以解决从管道取数据的阻塞问题


我们前面从一个管道里面去读取东西,需要close管道

如果不close管道我们for-range变量会阻塞,或者发生死锁

我们希望不关闭管道,也能正常退出的话,就使用"select"


package main

import (
"fmt"
"time"
)

func main(){
intChan := make(chan int,10)
for i := 0; i < 10;i++{
intChan<-i
}


stringChan := make(chan string,5)
for i := 0; i < 5;i++{
stringChan <- "hello" + fmt.Sprintf("%d",i)
}



//问题,在实际开发中,可能我们不好确定什么时候关闭该管道
//可以使用select 这种方式可以解决
for {
select {
case v := <-intChan: //注意,这里,如果intChan 一直没有关闭,也不会阻塞而死锁
//会自动到下一个case匹配
fmt.Printf("从intChan读取了数据%d\t\n",v)
time.Sleep(time.Second)

case v := <-stringChan:
fmt.Printf("从intChan读取了数据%s\t\n",v)
time.Sleep(time.Second)

default:
fmt.Println("都取不到了,程序员可以加入业务逻辑")
time.Sleep(time.Second)

//break 这里你是无法跳出循环的,这个会退出select,我们使用一个标签做退出
return //退出程序
}
}
}

  返回

从intChan读取了数据hello0 
从intChan读取了数据hello1
从intChan读取了数据hello2
从intChan读取了数据hello3
从intChan读取了数据0
从intChan读取了数据hello4
从intChan读取了数据1
从intChan读取了数据2
从intChan读取了数据3
从intChan读取了数据4
从intChan读取了数据5
从intChan读取了数据6
从intChan读取了数据7
从intChan读取了数据8
从intChan读取了数据9
都取不到了,程序员可以加入业务逻辑

//可以看到,他们读取了两个管道后,default就结束了

3 goroutine中使用recover,解决协程中出现panic,导致程序崩溃文件


当我们使用多个协程的时候,某一个协程出现panic时

会导致整个程序都崩溃,使用recover解决


package main

import (
"fmt"
"time"
)

func sayHello(){
for i := 0; i < 10; i++{
time.Sleep(time.Second)
fmt.Println("hello world")
}
}
func test(){
var myMap map[int]string
myMap[0] = "golang" //故意写错,因为map需要先make
}

func main(){
//我们这里直接起两个协程
go sayHello()
go test()
for i := 0 ; i<10;i++{
fmt.Println("OK")
time.Sleep(time.Second * 10)
}
}

返回

OK
panic: assignment to entry in nil map

goroutine 7 [running]:
main.test()
D:/go_setup/go1.17/src/go_code/go_pro/main/main.go:16 +0x25
created by main.main
D:/go_setup/go1.17/src/go_code/go_pro/main/main.go:22 +0x34

进程 已完成,退出代码为 2


panic: assignment to entry in nil map 键值对 给了一个nil,没有分配的空间

我们在开发中有个机制,当你有一个线程发生了panic 不要影响其他程序


recover 异常捕捉

package main

import (
"fmt"
"time"
)

func sayHello(){
for i := 0; i < 10; i++{
time.Sleep(time.Second)
fmt.Println("hello world")
}
}
func test(){
//这里我们可以使用defer + recover
defer func(){
//捕获test抛出的panic
if err := recover(); err != nil{
fmt.Println("test() 发生错误",err)
}
}()
var myMap map[int]string
myMap[0] = "golang"
}

func main(){
go sayHello()
go test()
for i := 0 ; i<10;i++{
fmt.Println("OK")
time.Sleep(time.Second * 10)
}
}

返回

OK
test() 发生错误 assignment to entry in nil map
hello world
hello world
hello world


会将panic进行捕捉并输出,不要让程序异常停止


说明

如果我们起了一个协程,但是这个协程出现了panic,如果我们没有捕获这个panic就会造成程序崩溃

这时,我们可以在gorouine中使用"recover"来捕获这个panic,进行处理

这样即使这个协程发生问题,但是主线程仍然不受影响,可以继续执行

八、channel阻塞机制


前面我们都是先写后读的,那么如果我们把readData读取函数注释了,会发生什么


案例1

package main

import (
"fmt"
"math/rand"
"time"
)

func writeData(intChan chan int) {
for i := 1; i <= 50; i++ {
intChan<- rand.Intn(50)
}
close(intChan)
}


func readData(intChan chan int,exitChan chan bool) {

for {
v , ok := <-intChan
if !ok {
break
}
fmt.Printf("readData 读取到数据=%v",v)
}

exitChan<- true
close(exitChan)

}

func main(){
rand.Seed(time.Now().UnixNano())
intChan := make(chan int,50)
exitChan := make(chan bool,1)

go writeData(intChan)
//go readData(intChan,exitChan) //如果讲读取的协程注释会发生什么

for{
_, ok := <-exitChan
if !ok{
break
}
}
}

返回

fatal error: all goroutines are asleep - deadlock!


如果只是向管道写入数据,而没有读取,就会出现阻塞而deadlock死锁,

因为intChan容量是10,而代码writeData会写入50个数据,因此会阻塞在writeData的ch<-i

只有当编辑器发现只有写入操作,没有任何读取操作的情况下则会发生死锁


案例2

package main

import (
"fmt"
"math/rand"
"time"
)

func writeData(intChan chan int) {
for i := 1; i <= 100; i++ { //因为我们这里默认写入的值是50个,和管道空间大小一致,不会超出,所以不会发生死锁,我们这里该大一点,改为100
intChan<- rand.Intn(50)
}
close(intChan)
}


func readData(intChan chan int,exitChan chan bool) {

for {
v , ok := <-intChan
if !ok {
break
}
fmt.Printf("readData 读取到数据=%v",v)
}

exitChan<- true
close(exitChan)

}

func main(){
rand.Seed(time.Now().UnixNano())
intChan := make(chan int,50)
exitChan := make(chan bool,1)

go writeData(intChan)
//go readData(intChan,exitChan) //如果讲读取的协程注释会发生什么

for{
_, ok := <-exitChan
if !ok{
break
}
}
}

返回

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]: //chan receive 管道无法关闭,程序发现这个管道永远无法关闭
//所以发生报错,因为他写不进去,所以永远写不完


goroutine 6 [chan send]: //第二个错误, 死锁
main.writeData(0x0) //错误函数main.writeData,第11行,写入到管道的数据没有读取操作
D:/go_setup/go1.17/src/test/main/main.go:11 +0x51

管道阻塞说明


如果编辑器在运行时发现一个管道只有写的操作,没有读的操作则该管道阻塞

另外关于管道的读写的频率不一致,这个没有关系的,因为他是异步的