我们上一章了解了什么是协程,我们在开始前先做一个练习
练习目标
我们现在要计算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呢?这就出现了共享变量并发导致竞争关系
竞争关系目前有两种解决方法 添加互斥锁和设置管道
一、互斥锁
互斥锁就是给变量进行加锁和解锁的操作,当我们要写入到变量数据时,
先去找这个锁,如果前面有数据在写入就加锁,如果前面写入完了就解锁
其他协程来找锁如果发现锁定了,就进入队列等待
在go中互斥锁由sync包提供, lock(加锁) unlock(解锁)
官方这里也说了,处理竞争关系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{},不过不推荐使用
队列示意图
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这个变量的内存地址
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方法
案例
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
管道阻塞说明
如果编辑器在运行时发现一个管道只有写的操作,没有读的操作则该管道阻塞
另外关于管道的读写的频率不一致,这个没有关系的,因为他是异步的