[go学习笔记.第十四章.协程和管道] 3.协程配合管道案例以及管道的注意事项和使用细节

案例一

请完成goroutine和channel协同工作的案例,具体要求:

(1).开启一个writeData协程,向管道intChan中写入50个整数.

(2).开启一个readData协程,从管道intChan中读取writeData写入的数据

(3).注意: writeData和readDate操作的是同一个管道

(4).主线程需要等待writeData和readDate协程都完成工作才能退出

 示例图如下:

代码如下: 

package main

import (
    "fmt"
)

//write data
func WriteData(intChan chan int) {
    for i := 0; i < 50; i++ {
        intChan <- i //放入数据
        fmt.Printf("write 数据=%v\n", i)
    }
    //关闭管道
    close(intChan)
}
//read data
func ReadData(intChan chan int, exitChan chan bool)  {
    for {
        v, ok := <-intChan
        if !ok {
            break
        }
        fmt.Printf("read 读取到的数据=%v\n", v)
    }
    //读取完成后,任务完成
    exitChan <- true
    close(exitChan)
}
func main()  {
    //创建两个管道
    intChan := make(chan int, 50)
    exitChan := make(chan bool, 1)
    go WriteData(intChan)
    go ReadData(intChan, exitChan)
    //判断
    for {
        _, ok := <- exitChan
        if !ok {
            break
        }
    }
}

阻塞

func main(){
intChan := make(chan int, 10) // 10->50的话数据一下就放入了 exitChan:=make(chan bool,1)

go writeData(intChan)

go readData(intChan,exitChan)

//就是为了等待..readData协程完成
for _ = range exitChan{
    fmt.Println("ok...") }
}

问题:

        如果注销掉 go readData(intChan,exitChan),程序会怎么样?

答:

        如果只是向管道写入数据,而没有读取(如果编译器(运行),发现一个管道只有写,而没有读,则该管道会阻塞;写管道和读管道的频率不一致,不影响),就会出现阻塞而dead lock,原因是intChan容量是10,而代码writeData会写入50个数据,因此会阻塞在writeData的 ch<-i

案例二

要求:

(1).启动一个协程,将1~2000的数放入到一个channel中,比如numChan

(2).启动8个协程,从numChan取出数(比如n),并计算1+...+n的值,并存放到resChan

(3).最后8个协呈协同完成工作后,再遍历resChan,显示结果[如res[1]=1..res[10]=55..]

注意:考虑resChan chan int是否合适?

案例三

goroutine+ channel配合完成排序,并写入文件

要求:

(1).开一个协程writeDataToFile,随机生成1000个数据,存放到文件中

(2).当writeDataToFile完成写1000个数据到文件后,让sort协程从文件中读取1000个文件,并完成排序,重新写入到另外一个文件

(3).考察点:协程和管道+文件的综合使用

(4).功能扩展:开10个协程writeDataToFile,每个协程随机生成1000个数据,存放到10文件中

(5).当10个文件都生成了,让10个sort协程从10文件中读取1000个文件,并完成排序,重新写入到10个结果文件

案例四

(1).创建一个 Person 结构体 [Name,Age,Address]

(2).使用rand方法配合随机创建10个Person实例,并放入到channel中

(3).遍历channel,将各个Person实例的信息显示在终端

package main

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

type Person struct {
    Name string
    Age int
    Address string 
}
//创建Person实例,并写入chan
func WriteChan(personChan chan Person)  {
    for i := 0; i < 100; i++ {
       //把person放入管道中
       personChan <- Person {
        Name : "Person" +  strconv.Itoa(rand.Intn(100)),
        Age : rand.Intn(100), 
        Address : "成都市华府大道"  +  strconv.Itoa(rand.Intn(100)) + "号",
       }
       fmt.Println("wtite data=", i)
    }
    //关闭管道
    close(personChan)
}
//读取chan数据,并显示到终端
func ReadChan(personChan chan Person, exitChan chan bool)  {
    i := 0
    for {
        i++
        v , ok := <- personChan
        if !ok {
            break
        }
        fmt.Printf("第%v个person数据的Name=%v,Age=%v, Address=%v\n", i,
        v.Name, v.Age, v.Address)   
    }
    读取完成后,任务完成
    exitChan <- true
    close(exitChan)
}
func main()  {
    //创建两个管道
    personChan := make(chan Person, 100)
    exitChan := make(chan bool, 1)
    go WriteChan(personChan)
    go ReadChan(personChan, exitChan)
    for {
        _, ok := <- exitChan
        if !ok {
            break
        }
    }
}

 案例五

需求:

        要求统计 1 ~ 200000 的数字中,哪些是素数?

分析思路:

        传统的方法,就是使用一个循环,循环的判断各个数是不是素数

使用并发并行的方式,将统计素数的任务分配给多个( 4 个) gorontine去完成,完成任务时间短

示意图如下: 

 代码如下: 

package main

import (
    "fmt"
    "time"
)

//向intChan放入1~8000
func putNum(intChan chan int)  {
    for i := 1; i <= 1000; i++ {
        intChan<- i 
    }
    //关闭intChan
    close(intChan)
}
//从intChan取出并判断是否素数,如果是,就放入primeChan中,当前协程完成后,需设置exitChan退出标识
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool)  {
    var flag bool // 是不是素数标识符
    for {
        time.Sleep(time.Millisecond * 10)
        //从intChan取出数据,并判断是不是素数
        num, ok := <-intChan
        if !ok {
            break
        }
        flag = true
        for i := 2; i < num; i++ {
            if  num % i == 0 { // 说明不是素数
                flag = false
                break
            }
        }
        if flag {
            //将结果放入primeChan
            primeChan<- num 
        }
    }
    fmt.Println("有一个primeChan取不到数据,退出")
    //在这里还不能关闭primeChan,因为不知道哪一个primeChan还在执行
    //向exitChan放入true
    exitChan<- true
}
func main() {
    //创建3个管道
    intChan := make(chan int, 1000) //存放1~8000的数
    primeChan := make(chan int, 1000) //放入结果
    exitChan := make(chan bool, 4) //标识退出的管道
    //开启一个协程,向intChan放入1~8000个数
    go putNum(intChan)
    //开启4个协程,从intChan取出并判断是否素数,如果是,就放入primeChan中
    for i := 1; i <= 4; i++ {
        go primeNum(intChan, primeChan, exitChan)
    }

    //判断primeChan是否执行完毕,并关闭prizeChan
    go func(){
        for i := 1; i <= 4; i++ {
            <-exitChan
        }   
        //当从exitChan中取出4个数后,就可以关闭primeChan了
        close(primeChan)
    }()
    //遍历primeChan
    for {
        res, ok := <-primeChan
        if !ok {
            break
        }
        //输出
        fmt.Printf("素数:%d\n", res)
    }
    fmt.Println("main主线程退出")
}

看看执行的速度:

package main

import (
    "fmt"
    "time"
)

//向intChan放入1~8000
func putNum(intChan chan int)  {
    for i := 1; i <= 80000; i++ {
        intChan<- i 
    }
    //关闭intChan
    close(intChan)
}
//从intChan取出并判断是否素数,如果是,就放入primeChan中,当前协程完成后,需设置exitChan退出标识
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool)  {
    var flag bool // 是不是素数标识符
    for {
        // time.Sleep(time.Millisecond * 10)
        //从intChan取出数据,并判断是不是素数
        num, ok := <-intChan
        if !ok {
            break
        }
        flag = true
        for i := 2; i < num; i++ {
            if  num % i == 0 { // 说明不是素数
                flag = false
                break
            }
        }
        if flag {
            //将结果放入primeChan
            primeChan<- num 
        }
    }
    fmt.Println("有一个primeChan取不到数据,退出")
    //在这里还不能关闭primeChan,因为不知道哪一个primeChan还在执行
    //向exitChan放入true
    exitChan<- true
}
func main() {
    //创建3个管道
    intChan := make(chan int, 1000) //存放1~8000的数
    primeChan := make(chan int, 1000) //放入结果
    exitChan := make(chan bool, 4) //标识退出的管道

    //开始时间
    start := time.Now().Unix()  
    //开启一个协程,向intChan放入1~8000个数
    go putNum(intChan)
    //开启4个协程,从intChan取出并判断是否素数,如果是,就放入primeChan中
    for i := 1; i <= 4; i++ {
        go primeNum(intChan, primeChan, exitChan)
    }

    //判断primeChan是否执行完毕,并关闭prizeChan
    go func(){
        for i := 1; i <= 4; i++ {
            <-exitChan
        }   
        //结束时间
        end := time.Now().Unix()    
        fmt.Println("使用协程耗费的时间:", end - start)
        //当从exitChan中取出4个数后,就可以关闭primeChan了
        close(primeChan)
    }()
    //遍历primeChan
    for {
        _, ok := <-primeChan
        if !ok {
            break
        }
        //输出
        //fmt.Printf("素数:%d\n", res)
    }
    fmt.Println("main主线程退出")
}

 结论:使用go协程后,执行速度比普通方法提高至少4倍

注意事项

1.管道可以声明为只读或只写

package main

import (
    "fmt"
)

func main() {
    //管道可以声明为只读或只写
    //1.在默认的情况下,管道是双向的
    // var chan1 chan int//可读可写
    //2.声明为只写
    var chan2 chan<- int
    chan2 = make(chan int, 3)
    chan2<- 33
    // num := <-chan2 //error
    fmt.Println("chan2=", chan2)
    //2.声明为只读
    var chan3 <-chan int
    chan3 = make(chan int, 3)
    num := <-chan3 
    //chan3<- 33//error
    
    fmt.Println("num=", num)
}

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

package main

import (
    "fmt"
)

func main()  {
    //使用select可以解决从管道取数据的阻塞问题
    //1.定义一个管道 10个数据int
    intChan := make(chan int, 10)
    for i := 0; i < 10; i++ {
        intChan <- i 
    }
    //2.定义一个管道 5个数据string   
    strChan := make(chan string, 5)
    for i := 0; i < 5; i++ {
        strChan <- "hello" + fmt.Sprintf("%d", i) 
    }

    //传统的方式在遍历管道时,如果不关闭会阻塞而导致deallock
    
    //问题:在实际开发中,有可能不好确定在什么情况下去关闭管道
    //可以使用select方式解决
    for {
        select {
            //注意,如果intChan一直没有关闭,不会一直阻塞而deallock,会自动到下一个case匹配
        case v := <- intChan :
            fmt.Printf("从intChan取出数据:%v\n", v)
        case v := <- strChan :
            fmt.Printf("从strChan取出数据:%s\n", v)
        default :
            fmt.Println("取不到数据了,程序员可以添加自己的逻辑代码")
            return
        }
    }
}

3.goroutine 中使用 recover 解决协程中出现 panic导致程序崩溃问题

        如果我们起了一个协程,但是这个协程出现了panic,如果我们没有捕获这个panic,就会造成整个程序崩溃,这时我们可以在 goroutine 中使用 recover来捕获 panic ,进行处理,这样即使这个协程发生的问题,但是主线程仍然不受影响,可以继续执行

package main

import (
    "fmt"
    "time"
)

func sayHello() {
    for i := 0; i < 10; i++ {
        time.Sleep(time.Second)
        fmt.Println("hello")
    }
}

func test()  {
    //这里可以使用defer + recover
    defer func ()  {
        //捕获test抛出的异常
        if err := recover(); err != nil {
            fmt.Println("test() 发生错误", err)
        }
    }()
    var map1 map[int]string
    map1[0] = "go"  //error
}

func main()  {
    go sayHello()
    go test()

    for i := 0; i < 10; i++ {
        fmt.Println("main hi")
        time.Sleep(time.Second)
    }
}

[上一节][go学习笔记.第十四章.协程和管道] 2.管道

[下一节][go学习笔记.第十五章.反射] 1.反射的基本介绍以及实践

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值