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


    案例一

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

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

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

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

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

     示例图如下:

    代码如下: 

    1. package main
    2. import (
    3. "fmt"
    4. )
    5. //write data
    6. func WriteData(intChan chan int) {
    7. for i := 0; i < 50; i++ {
    8. intChan <- i //放入数据
    9. fmt.Printf("write 数据=%v\n", i)
    10. }
    11. //关闭管道
    12. close(intChan)
    13. }
    14. //read data
    15. func ReadData(intChan chan int, exitChan chan bool) {
    16. for {
    17. v, ok := <-intChan
    18. if !ok {
    19. break
    20. }
    21. fmt.Printf("read 读取到的数据=%v\n", v)
    22. }
    23. //读取完成后,任务完成
    24. exitChan <- true
    25. close(exitChan)
    26. }
    27. func main() {
    28. //创建两个管道
    29. intChan := make(chan int, 50)
    30. exitChan := make(chan bool, 1)
    31. go WriteData(intChan)
    32. go ReadData(intChan, exitChan)
    33. //判断
    34. for {
    35. _, ok := <- exitChan
    36. if !ok {
    37. break
    38. }
    39. }
    40. }

    阻塞

    1. func main(){
    2. intChan := makechan int10// 10->50的话数据一下就放入了 exitChan:=make(chan bool,1)
    3. go writeData(intChan)
    4. go readData(intChan,exitChan)
    5. //就是为了等待..readData协程完成
    6. for _ = range exitChan{
    7. fmt.Println("ok...") }
    8. }

    问题:

            如果注销掉 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实例的信息显示在终端

    1. package main
    2. import (
    3. "fmt"
    4. "math/rand"
    5. "strconv"
    6. )
    7. type Person struct {
    8. Name string
    9. Age int
    10. Address string
    11. }
    12. //创建Person实例,并写入chan
    13. func WriteChan(personChan chan Person) {
    14. for i := 0; i < 100; i++ {
    15. //把person放入管道中
    16. personChan <- Person {
    17. Name : "Person" + strconv.Itoa(rand.Intn(100)),
    18. Age : rand.Intn(100),
    19. Address : "成都市华府大道" + strconv.Itoa(rand.Intn(100)) + "号",
    20. }
    21. fmt.Println("wtite data=", i)
    22. }
    23. //关闭管道
    24. close(personChan)
    25. }
    26. //读取chan数据,并显示到终端
    27. func ReadChan(personChan chan Person, exitChan chan bool) {
    28. i := 0
    29. for {
    30. i++
    31. v , ok := <- personChan
    32. if !ok {
    33. break
    34. }
    35. fmt.Printf("第%v个person数据的Name=%v,Age=%v, Address=%v\n", i,
    36. v.Name, v.Age, v.Address)
    37. }
    38. 读取完成后,任务完成
    39. exitChan <- true
    40. close(exitChan)
    41. }
    42. func main() {
    43. //创建两个管道
    44. personChan := make(chan Person, 100)
    45. exitChan := make(chan bool, 1)
    46. go WriteChan(personChan)
    47. go ReadChan(personChan, exitChan)
    48. for {
    49. _, ok := <- exitChan
    50. if !ok {
    51. break
    52. }
    53. }
    54. }

     案例五

    需求:

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

    分析思路:

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

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

    示意图如下: 

     代码如下: 

    1. package main
    2. import (
    3. "fmt"
    4. "time"
    5. )
    6. //向intChan放入1~8000
    7. func putNum(intChan chan int) {
    8. for i := 1; i <= 1000; i++ {
    9. intChan<- i
    10. }
    11. //关闭intChan
    12. close(intChan)
    13. }
    14. //从intChan取出并判断是否素数,如果是,就放入primeChan中,当前协程完成后,需设置exitChan退出标识
    15. func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
    16. var flag bool // 是不是素数标识符
    17. for {
    18. time.Sleep(time.Millisecond * 10)
    19. //从intChan取出数据,并判断是不是素数
    20. num, ok := <-intChan
    21. if !ok {
    22. break
    23. }
    24. flag = true
    25. for i := 2; i < num; i++ {
    26. if num % i == 0 { // 说明不是素数
    27. flag = false
    28. break
    29. }
    30. }
    31. if flag {
    32. //将结果放入primeChan
    33. primeChan<- num
    34. }
    35. }
    36. fmt.Println("有一个primeChan取不到数据,退出")
    37. //在这里还不能关闭primeChan,因为不知道哪一个primeChan还在执行
    38. //向exitChan放入true
    39. exitChan<- true
    40. }
    41. func main() {
    42. //创建3个管道
    43. intChan := make(chan int, 1000) //存放1~8000的数
    44. primeChan := make(chan int, 1000) //放入结果
    45. exitChan := make(chan bool, 4) //标识退出的管道
    46. //开启一个协程,向intChan放入1~8000个数
    47. go putNum(intChan)
    48. //开启4个协程,从intChan取出并判断是否素数,如果是,就放入primeChan中
    49. for i := 1; i <= 4; i++ {
    50. go primeNum(intChan, primeChan, exitChan)
    51. }
    52. //判断primeChan是否执行完毕,并关闭prizeChan
    53. go func(){
    54. for i := 1; i <= 4; i++ {
    55. <-exitChan
    56. }
    57. //当从exitChan中取出4个数后,就可以关闭primeChan了
    58. close(primeChan)
    59. }()
    60. //遍历primeChan
    61. for {
    62. res, ok := <-primeChan
    63. if !ok {
    64. break
    65. }
    66. //输出
    67. fmt.Printf("素数:%d\n", res)
    68. }
    69. fmt.Println("main主线程退出")
    70. }

    看看执行的速度:

    1. package main
    2. import (
    3. "fmt"
    4. "time"
    5. )
    6. //向intChan放入1~8000
    7. func putNum(intChan chan int) {
    8. for i := 1; i <= 80000; i++ {
    9. intChan<- i
    10. }
    11. //关闭intChan
    12. close(intChan)
    13. }
    14. //从intChan取出并判断是否素数,如果是,就放入primeChan中,当前协程完成后,需设置exitChan退出标识
    15. func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
    16. var flag bool // 是不是素数标识符
    17. for {
    18. // time.Sleep(time.Millisecond * 10)
    19. //从intChan取出数据,并判断是不是素数
    20. num, ok := <-intChan
    21. if !ok {
    22. break
    23. }
    24. flag = true
    25. for i := 2; i < num; i++ {
    26. if num % i == 0 { // 说明不是素数
    27. flag = false
    28. break
    29. }
    30. }
    31. if flag {
    32. //将结果放入primeChan
    33. primeChan<- num
    34. }
    35. }
    36. fmt.Println("有一个primeChan取不到数据,退出")
    37. //在这里还不能关闭primeChan,因为不知道哪一个primeChan还在执行
    38. //向exitChan放入true
    39. exitChan<- true
    40. }
    41. func main() {
    42. //创建3个管道
    43. intChan := make(chan int, 1000) //存放1~8000的数
    44. primeChan := make(chan int, 1000) //放入结果
    45. exitChan := make(chan bool, 4) //标识退出的管道
    46. //开始时间
    47. start := time.Now().Unix()
    48. //开启一个协程,向intChan放入1~8000个数
    49. go putNum(intChan)
    50. //开启4个协程,从intChan取出并判断是否素数,如果是,就放入primeChan中
    51. for i := 1; i <= 4; i++ {
    52. go primeNum(intChan, primeChan, exitChan)
    53. }
    54. //判断primeChan是否执行完毕,并关闭prizeChan
    55. go func(){
    56. for i := 1; i <= 4; i++ {
    57. <-exitChan
    58. }
    59. //结束时间
    60. end := time.Now().Unix()
    61. fmt.Println("使用协程耗费的时间:", end - start)
    62. //当从exitChan中取出4个数后,就可以关闭primeChan了
    63. close(primeChan)
    64. }()
    65. //遍历primeChan
    66. for {
    67. _, ok := <-primeChan
    68. if !ok {
    69. break
    70. }
    71. //输出
    72. //fmt.Printf("素数:%d\n", res)
    73. }
    74. fmt.Println("main主线程退出")
    75. }

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

    注意事项

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

    1. package main
    2. import (
    3. "fmt"
    4. )
    5. func main() {
    6. //管道可以声明为只读或只写
    7. //1.在默认的情况下,管道是双向的
    8. // var chan1 chan int//可读可写
    9. //2.声明为只写
    10. var chan2 chan<- int
    11. chan2 = make(chan int, 3)
    12. chan2<- 33
    13. // num := <-chan2 //error
    14. fmt.Println("chan2=", chan2)
    15. //2.声明为只读
    16. var chan3 <-chan int
    17. chan3 = make(chan int, 3)
    18. num := <-chan3
    19. //chan3<- 33//error
    20. fmt.Println("num=", num)
    21. }

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

    1. package main
    2. import (
    3. "fmt"
    4. )
    5. func main() {
    6. //使用select可以解决从管道取数据的阻塞问题
    7. //1.定义一个管道 10个数据int
    8. intChan := make(chan int, 10)
    9. for i := 0; i < 10; i++ {
    10. intChan <- i
    11. }
    12. //2.定义一个管道 5个数据string
    13. strChan := make(chan string, 5)
    14. for i := 0; i < 5; i++ {
    15. strChan <- "hello" + fmt.Sprintf("%d", i)
    16. }
    17. //传统的方式在遍历管道时,如果不关闭会阻塞而导致deallock
    18. //问题:在实际开发中,有可能不好确定在什么情况下去关闭管道
    19. //可以使用select方式解决
    20. for {
    21. select {
    22. //注意,如果intChan一直没有关闭,不会一直阻塞而deallock,会自动到下一个case匹配
    23. case v := <- intChan :
    24. fmt.Printf("从intChan取出数据:%v\n", v)
    25. case v := <- strChan :
    26. fmt.Printf("从strChan取出数据:%s\n", v)
    27. default :
    28. fmt.Println("取不到数据了,程序员可以添加自己的逻辑代码")
    29. return
    30. }
    31. }
    32. }

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

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

    1. package main
    2. import (
    3. "fmt"
    4. "time"
    5. )
    6. func sayHello() {
    7. for i := 0; i < 10; i++ {
    8. time.Sleep(time.Second)
    9. fmt.Println("hello")
    10. }
    11. }
    12. func test() {
    13. //这里可以使用defer + recover
    14. defer func () {
    15. //捕获test抛出的异常
    16. if err := recover(); err != nil {
    17. fmt.Println("test() 发生错误", err)
    18. }
    19. }()
    20. var map1 map[int]string
    21. map1[0] = "go" //error
    22. }
    23. func main() {
    24. go sayHello()
    25. go test()
    26. for i := 0; i < 10; i++ {
    27. fmt.Println("main hi")
    28. time.Sleep(time.Second)
    29. }
    30. }

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

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

  • 相关阅读:
    DataGrip 2024 po for Mac 数据库管理工具解
    第4章_freeRTOS入门与工程实践之开发板使用
    Flink从Kafka写入mysql
    Django model 联合约束和联合索引
    TCP/IP(十七)实战抓包分析(一)ICMP
    PostgreSQL数据优化——死元组清理
    GDB使用详解
    Qt学习总结之QComboBox
    node-rsa公钥私钥加密解密
    源码分享-M3U8数据流ts的AES-128解密并合并---GoLang实现
  • 原文地址:https://blog.csdn.net/zhoupenghui168/article/details/127853770