goroutine 和 channel

goroutine

看一个需求

  1. 要求统计1-20000的数字中,哪些是素数?

    • 分析思路:
      • 传统的方法,就是使用一个循环,循环的判断各个数是不是素数。
      • 使用并发或者并行的方式,将统计素数的任务分配给多个 goroutine 去完成,这时就会使用到 goroutine。

        基本介绍

        进程和线程的说明

  2. 进程就是程序在操作系统中一次执行过程,是系统进行资源分配和调度的基本单位。

  3. 线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位。
  4. 一个进程可以创建销毁多个线程,同一个进程的多个线程可以并发执行。
  5. 一个程序至少有一个进程,一个进程,至少有一个线程。

    并发和并行

  6. 多线程程序在单核上运行,就是并发。

    • 多个任务作用在一个 cpu
    • 从微观的角度看,在一个时间点上,其实只有一个任务在执行。
  7. 多线程程序在多核上运行,就是并行。

    • 多个任务作用在多个cpu
    • 从微观的角度看,在一个时间点上,就是多个任务在同时执行。
    • 这样看来,并行的速度快。

      Go 协程和 Go 主线程

  8. Go 主线程(由程序员直接成为线程/也可以理解为进程):一个 Go 线程上,可多个协程,你可以这样理解,协程是轻量级的线程。

  9. Go 协程的特点

    • 有独立的栈空间
    • 共享程序堆空间
    • 调度由用户控制
    • 协程是轻量级的线程

      快速入门

  10. 编写一个程序,完成如下功能:

    • 在主线程(可以理解成进程)中,开启一个 goroutine,该协程每隔 1 秒输出 “hello, world!”
    • 在主线程中也每隔一秒输出 “hello,golang”,输出 10 次后,退出程序
    • 要求主线程和 goroutine 同时执行。 ```go package main

import ( “fmt” “strconv” “time” )

/ 案例: (1) 在主线程(可以理解成进程)中,开启一个 goroutine,该协程每隔1秒输出”heello, world!” (2) 在主线程中也每隔一秒输出”hello,golang”,输出10次后,退出程序 (3) 要求主线程和 goroutine 同时执行。 /

//编写一个函数,每隔1秒输出 “hello,world” func test() { for i :=0; i <= 10; i++ { fmt.Println(“test() hello, world “ + strconv.Itoa(i)) time.Sleep(time.Second) } }

func main() {

  1. go test() //开启了一个协程
  2. for i := 0; i <= 10; i++ {
  3. fmt.Println("main() hello, golang " + strconv.Itoa(i))
  4. time.Sleep(time.Second)
  5. }

}

  1. 2. 总结
  2. - 如果主线程退出了,则协程即使还没有执行完毕,也会退出。
  3. - 当然协程也可以在主线程没有退出前,就自己结束了,比如完成了自己的任务。
  4. 3. 快速入门案例小结
  5. - 主线程是一个物理进程,直接作用在 cpu 上的,是重量级的,非常耗费 cpu 资源。
  6. - 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
  7. - Golang 的协程机制是重要的特点,可以轻松的开启上万个协程。其他编程语言的并发机制是一般基于线程的,开启过多线程,资源耗费大,这里就突显 Golang 在并发上的优势了。
  8. <a name="6853de89"></a>
  9. ### goroutine 的调度模型
  10. <a name="eabb2d63"></a>
  11. #### MPG 模型基本介绍
  12. 1. MPG 模型基本介绍
  13. - M: 操作系统的主线程(物理进程)
  14. - P: 协程执行需要的上下文
  15. - G: 协程
  16. 2. MPG 模式运行的状态一
  17. - 当前程序有三个 M ,如果三个 M 都在一个 CPU 运行,就是并发,如果在不同的 CPU 运行就是并行。
  18. - M1, M2, M3 正在执行一个 GM1 的协程队列有三个,M2 的协程队列有 3 个,M3 协程队列有 2 个。
  19. - Go 的协程是轻量级的线程,是逻辑态的,Go 可以容易的起上万个协程。
  20. - 其他程序 C/Java 的多线程,往往是内核态的,比较重量级,几千个线程可能耗光 CPU
  21. 3. MPG 模式运行的状态二
  22. - M0 主线程正在执行 G0 协程,另外有三个协程在队列等待
  23. - 如果 G0 协程阻塞,比如读取文件或者数据库等。
  24. - 这时就会创建 M1 主线程(也可能是从已有的线程池中取出 M1),并且将等待的3个协程挂到 M1 下执行,M0 的主线程下的 G0 仍然执行文件 io 的读写。
  25. - 这样的 MPG 调度模式,可以既让 G0 执行,同时也不会让队列的其他协程一直堵塞,仍然可以并发/并行执行。
  26. - 等到 G0 不再堵塞了,M0 会被放到空闲的主线程继续执行(从已有的线程池中取),同时 G0 又会被唤醒。
  27. <a name="d8993da2"></a>
  28. ### 设置 Golang 运行的 CPU 数目
  29. 1. 设置 Golang 运行的 CPU 数目
  30. ```go
  31. package main
  32. import (
  33. "fmt"
  34. "runtime"
  35. )
  36. func main() {
  37. cpuNum := runtime.NumCPU()
  38. fmt.Println("cpuNum = ", cpuNum)
  39. //可以自己设置使用多少个 cpu
  40. runtime.GOMAXPROCS(cpuNum - 2)
  41. fmt.Println("ok")
  42. }

channel(管道)

看个需求

  1. 需求:现在要计算1-200的各个数的阶乘,并且把各个数的阶乘放入 map 中,最后显示出来。要求使用 goroutine 完成。
  2. 分析思路:
    • 使用 goroutine 来完成,效率高,但是会出现并发/并行安全问题。
    • 这里就提出了不同 goroutine 如何通信的问题
  3. 代码实现
    • 使用 goroutine 来完成
    • 在运行某个程序时,在编译该程序时候,可以使用 go build -race xxx.go ,就会知道是否存在资源竞争问题。
  4. 代码描述 ```go package main

import ( “fmt” “time” )

/ 案例一: 需求:现在要计算1-200的各个数的阶乘,并且把各个数的阶乘放入 map 中,最后显示出来。 要求使用 goroutine 完成。 /

//思路 //1. 编写一个函数,来计算各个数的阶乘,并放入到 map 中。 //2. 我们启动的协程多个,统一的将阶乘的结果放入到 map 中。 //3. map 应该做成一个全局的。

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

// test 函数就是计算 n!,然后将这个结果放入 myMap func test(n int) { res := 1 for i:=1; i<= n; i++ { res *= i }

  1. //这里我们将 res 放入到 myMap 中
  2. myMap[n] = res // fatal error: concurrent map writes -- 此段代码报错

}

func main() { //这里开启多个协程完成任务 for i :=1; i <= 200; i++ { go test(i) }

  1. //休眠 10s
  2. time.Sleep(time.Second * 10)
  3. //输出结果,遍历这个结果
  4. for index, value := range myMap {
  5. fmt.Printf("map[%d] = %d \n", index, value)
  6. }

}

  1. <a name="4a162c5d"></a>
  2. ### 不同 goroutine 之间如何通讯
  3. 1. 全局变量的互斥锁
  4. 1. 使用管道 channel 来解决
  5. <a name="834b9c09"></a>
  6. ### 使用全局变量加锁同步改进程序
  7. 1. 代码改进
  8. ```go
  9. package main
  10. import (
  11. "fmt"
  12. "sync"
  13. "time"
  14. )
  15. /*
  16. 案例一:
  17. 需求:现在要计算1-200的各个数的阶乘,并且把各个数的阶乘放入 map 中,最后显示出来。
  18. 要求使用 goroutine 完成。
  19. */
  20. /*
  21. 思路:
  22. 1. 编写一个函数,来计算各个数的阶乘,并放入到 map 中。
  23. 2. 我们启动的协程多个,统一的将阶乘的结果放入到 map 中。
  24. 3. map 应该做成一个全局的。
  25. */
  26. var (
  27. myMap = make(map[int]int, 10)
  28. //声明一个全局的互斥锁
  29. //lock 是一个全局的互斥锁
  30. //sync 是包:syncchornized 同步
  31. //Mutex : 互斥
  32. lock sync.Mutex
  33. )
  34. // test 函数就是计算 n!,然后将这个结果放入 myMap
  35. func test(n int) {
  36. res := 1
  37. for i:=1; i <= n; i++ {
  38. res *= i
  39. }
  40. //这里我们将 res 放入到 myMap 中
  41. //加锁
  42. lock.Lock()
  43. myMap[n] = res // fatal error: concurrent map writes -- 此段代码报错
  44. //解锁
  45. lock.Unlock()
  46. }
  47. func main() {
  48. //这里开启多个协程完成任务
  49. for i := 1; i <= 20; i++ {
  50. go test(i)
  51. }
  52. //休眠 10s
  53. time.Sleep(time.Second * 5)
  54. //输出结果,遍历这个结果
  55. lock.Lock()
  56. for index, value := range myMap {
  57. fmt.Printf("map[%d] = %d \n", index, value)
  58. }
  59. lock.Unlock()
  60. }

基本介绍

为什么需要 channel

  1. 前面使用全局变量加锁同步来解决 goroutine 的通讯,但不完美。
  2. 主线程在等待所有 goroutine 全部完成的时间很难确定,这里我们设置10秒,仅仅是估算。
  3. 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有 goroutine 处于工作状态,这是也会随着主线程的退出而销毁。
  4. 通过全局变量加锁同步来实现通讯,也并不利于多个协程对全局变量的读写操作。

    channel 的介绍

  5. channel 本质就是一个数据结构-队列

  6. 数据是先进先出
  7. 线程安全,多 goroutine 访问时,不需要加锁,就是说 channel 本身就是线程安全的。
  8. channel 时有类型,一个 string 的 channel 只能存放 string 类型数据。

    基本使用

    定义/声明变量 channel

  9. 举例

    1. var intChan chan int (intChan 用于存放 int 数据)
    2. var mapChan chan map[int]string (mapChan 用于存放 map[int]string 类型)
    3. var perChan chan Person
    4. var perChan2 chan *Person
    5. ...
  10. 说明

    • channel 是引用类型
    • channel 必须初始化才能写入数据,即 make 后才能使用
    • 管道是有类型的,intChan 只能写入整数 int
  11. channel 初始化

    1. var intChan chan int
    2. intChan = make(chan int, 10)
  12. channel 写入数据

    1. var intChan chan int
    2. intChan = make(chan int, 10)
    3. num := 999
    4. intChan <- 10
    5. intChan <- num
  13. 快速入门案例 ```go package main

import “fmt”

func main() { //演示一下管道的使用 //1. 创建一个可以存放3个 int 类型的管道 var intChan chan int intChan = make(chan int, 3)

  1. //2. 看看 intChan 是什么
  2. fmt.Printf("intChan 的值 = %v intChan 本身的地址 = %p \n", intChan, &intChan) // 地址
  3. //3. 向管道写入数据
  4. intChan <- 10
  5. num := 211
  6. intChan <- num
  7. //注意点,当给我们给管道写入数据时,不能超过其容量
  8. intChan <- 50
  9. //intChan <- 98 // deadlock!
  10. //4. 看看管道的长度和cap(容量)
  11. fmt.Printf("intChan len = %v cap = %v \n", len(intChan), cap(intChan))
  12. //5. 从管道中读取数据
  13. var num2 int
  14. num2 = <- intChan
  15. fmt.Println("num2 =", num2)
  16. fmt.Printf("intChan len = %v cap = %v \n", len(intChan), cap(intChan))
  17. //6. 在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报告 deadlock!
  18. num3 := <-intChan
  19. num4 := <-intChan
  20. // num5 := <-intChan 报错
  21. fmt.Println("num3 =", num3, "num4 =", num4)

}

  1. <a name="4d62e1f8"></a>
  2. #### channel 使用的注意事项
  3. 1. channel 使用的注意事项
  4. - channel 中只能存放指定的数据类型
  5. - channel 的数据放满后,就不能再放入了。
  6. - 如果从 channel 取出数据后,可以继续放入。
  7. - 在没有使用协程的情况下,如果 channel 数据取完了,再取就会报告 dead lock。
  8. <a name="24f60c15"></a>
  9. #### 案例演示
  10. 1. 案例一
  11. ```go
  12. package main
  13. import "fmt"
  14. func main() {
  15. var intChan chan int
  16. intChan = make(chan int, 3)
  17. intChan <- 10
  18. intChan <- 20
  19. intChan <- 10
  20. //因为 intChan 的容量为3,再存会报告 dead lock!
  21. //intChan <- 50
  22. num1 := <- intChan
  23. num2 := <- intChan
  24. num3 := <- intChan
  25. //因为 intChan 这是已经没有数据了,再取就会报告 dead lock
  26. //num4 := <- intChan
  27. fmt.Printf("num1 = %v num2 = %v, num3 = %v", num1, num2, num3)
  28. }
  1. 案例二 ```go 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”] = “天津”

  1. m2 := make(map[string]string, 20)
  2. m2["hero1"] = "宋江"
  3. m2["hero2"] = "武松"
  4. //将数据存放到 mapChan
  5. mapChan <- m1
  6. mapChan <- m2
  7. //取出
  8. m3 := <- mapChan
  9. m4 := <- mapChan
  10. fmt.Printf("mapChan = %v, mapChan = %p \n", mapChan, &mapChan)
  11. fmt.Println(m3, m4)

}

  1. 3.案例三
  2. ```go
  3. package main
  4. import "fmt"
  5. type Cat struct {
  6. Name string
  7. Age int
  8. }
  9. func main() {
  10. var catChan chan Cat
  11. catChan = make(chan Cat, 10)
  12. cat1 := Cat{Name: "Tom", Age: 18,}
  13. cat2 := Cat{Name: "Tom~", Age: 180,}
  14. catChan <- cat1
  15. catChan <- cat2
  16. //取出
  17. cat11 := <- catChan
  18. cat22 := <- catChan
  19. fmt.Println(cat11, cat22)
  20. }
  1. 案例四 ```go package main

import “fmt”

type Cat struct { Name string Age int }

func main() { var catChan chan Cat catChan = make(chan Cat, 10)

  1. cat1 := Cat{Name: "Tom", Age: 18,}
  2. cat2 := Cat{Name: "Tom~", Age: 180,}
  3. catChan <- &cat1
  4. catChan <- &cat2
  5. //取出
  6. cat11 := <- catChan
  7. cat22 := <- catChan
  8. fmt.Println(cat11, cat22)
  9. fmt.Println(*cat11, *cat22)

}

  1. 5. 案例五
  2. ```go
  3. package main
  4. import "fmt"
  5. type Cat struct {
  6. Name string
  7. Age int
  8. }
  9. func main() {
  10. var allChan chan interface{}
  11. allChan = make(chan interface{}, 10)
  12. cat1 := Cat{Name: "Tom", Age: 18,}
  13. cat2 := Cat{Name: "Tom~", Age: 180,}
  14. allChan <- cat1
  15. allChan <- cat2
  16. allChan <- 10
  17. allChan <- "jack"
  18. //取出
  19. cat11 := <- allChan
  20. cat22 := <- allChan
  21. v1 := <- allChan
  22. v2 := <- allChan
  23. fmt.Println(cat11, cat22, v1, v2)
  24. }
  1. 案例六 ```go package main

import “fmt”

type Cat struct { Name string Age int }

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

  1. cat1 := Cat{Name: "Tom", Age: 18,}
  2. cat2 := Cat{Name: "Tom~", Age: 180,}
  3. allChan <- cat1
  4. allChan <- cat2
  5. allChan <- 10
  6. allChan <- "jack"
  7. //取出
  8. cat11 := <- allChan
  9. fmt.Printf("cat11 = %T cat11 = %v \n", cat11, cat11)
  10. // fmt.Println(cat11.Name) 报错,需要用到类型断言
  11. cat33 := cat11.(Cat)
  12. fmt.Println(cat33.Name)

}

  1. <a name="cfb614ed"></a>
  2. ### channel 的遍历和关闭
  3. <a name="2191bc5a"></a>
  4. #### channel 的关闭
  5. 1. 使用内置函数 close 可以关闭 channel,当 channel 关闭后,就不能再向channel 写入数据了,但是仍然可以从该 channel 读取数据。
  6. 1. 案例演示
  7. ```go
  8. package main
  9. import "fmt"
  10. func main() {
  11. intChan := make(chan int, 3)
  12. intChan <- 100
  13. intChan <- 200
  14. close(intChan) //close
  15. //这是不能够再写入数到 channel
  16. //intChan <- 300
  17. //fmt.Println("okok~")
  18. //当管道关闭后,读取数据是可以的
  19. n1 := <- intChan
  20. fmt.Println("n1 =", n1)
  21. }

channel 的遍历

  1. channel 支持 for-range 的方式进行遍历,请注意两个细节
    • 在遍历时,如果 channel 没有关闭,则会出现 deadlock 的错误。
    • 在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
  2. 案例演示 ```go package main

import “fmt”

func main() { //遍历管道 intChan := make(chan int, 100) for i := 0; i < 100; i++ { intChan <- i * 2 //放入100个数据到管道 }

  1. //遍历管道不能使用普通的 for 循环结构
  2. //在遍历时,如果 channel 没有关闭,则会出现 deadlock 的错误。
  3. close(intChan) //关闭管道
  4. for value := range intChan {
  5. fmt.Println("value = ", value)
  6. }

}

  1. <a name="1c689c03"></a>
  2. #### goroutine 和 channel 结合
  3. 1. 应用案例
  4. - 开启一个 writeData 协程,向管道 intChan 中写入 50 个整数。
  5. - 开启一个readData 协程,从管道 intChan 中读取 writeData 写入的数据。
  6. - 注意:writeData 和 readData 操作的是同一个管道
  7. - 主线程需要等待 writeData 和 readData 协程都完成工作才能退出。
  8. ```go
  9. package main
  10. import (
  11. "fmt"
  12. )
  13. /*
  14. 案例要求:
  15. (1) 开启一个 writeData 协程,向管道 intChan 中写入50个整数。
  16. (2) 开启一个readData 协程,从管道 intChan 中读取 writeData 写入的数据。
  17. (3) 注意:writeData 和 readData 操作的是同一个管道
  18. (4) 主线程需要等待 writeData 和 readData 协程都完成工作才能退出。
  19. */
  20. func writeData(intChan chan int) {
  21. for i := 1; i <= 50; i++ {
  22. //放入数据
  23. intChan <- i
  24. fmt.Println("writeData value =", i)
  25. //time.Sleep(time.Second)
  26. }
  27. close(intChan) // close()
  28. }
  29. func readData(intChan chan int, exitChan chan bool) {
  30. for {
  31. value , ok := <- intChan
  32. if !ok {
  33. break
  34. } else {
  35. //time.Sleep(time.Second)
  36. fmt.Println("readData() value =", value)
  37. }
  38. }
  39. //readData 读取完数据后,即任务完成
  40. exitChan <- true
  41. close(exitChan)
  42. }
  43. func main() {
  44. //创建两个管道
  45. intChan := make(chan int, 50)
  46. exitChan := make(chan bool, 3)
  47. go writeData(intChan)
  48. go readData(intChan, exitChan)
  49. for {
  50. _, ok := <- exitChan
  51. if !ok {
  52. break
  53. }
  54. }
  55. }
  1. 应用案例

    1. func main() {
    2. intChan := make(chan int, 10) //50->10
    3. exitChan := make(chan bool, 1)
    4. go writeData(intChan)
    5. go readData(intChan, exitChan)
    6. //就是为了等待...readData 协程完成
    7. for _ := range exitChan {
    8. fmt.Println("ok...")
    9. }
    10. }
  2. 对上面问题分析

    • 对上面 go readData(intChan, exitChan) 注销,会造成 dead lock!
    • 如果只向管道写入数据,而没有读取,就会出现阻塞而 dead lock,原因是 intChan 容量是10,而代码 writeData 会写入50个数据,因此会阻塞。
    • 如果编译器(运行),只有写,而没有毒,则该管道会堵塞;如果写管道和读管道的频率不一致,无所谓。
  3. 应用实例
    • 需求:要求统计1-80000的数字中,哪些是素数?
      • 分析思路:
        • 传统的方法,就是使用一个循环,循环的判断各个数是不是素数。
          • 使用并发/并行的方式,将统计素数的任务分配给多个(4个) goroutine 去完成,完成任务时间短。 ```go package main

import ( “fmt” “time” )

// intChan 放入 1-8000个数 func putNum(intChan chan int) { for i := 0; i < 8000; i++ { intChan <- i }

  1. //关闭 intChan
  2. close(intChan)

}

// 从 intChan 取出数据,并判断是否为素数,如果是,就放入 primeChan func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {

  1. // 使用 for 循环
  2. var flag bool
  3. for {
  4. time.Sleep(time.Microsecond)
  5. num, ok := <- intChan
  6. if !ok { //intChan 取不到
  7. break
  8. }
  9. flag = true //假设是素数
  10. //判断 num 是不是素数
  11. for i := 2; i < num; i++ {
  12. if num % i == 0{
  13. //说明该 num 不是素数
  14. flag = false
  15. break
  16. }
  17. }
  18. if flag {
  19. //将这个数放入到 primeChan
  20. primeChan <- num
  21. }
  22. }
  23. fmt.Println("有一个 primeNum 协程取不到数据,退出")
  24. //这里我们还不能关闭 primeChan
  25. //向 exitChan 写入 true
  26. exitChan <- true

}

func main() {

  1. intChan := make(chan int, 1000)
  2. primeChan := make(chan int, 2000) //放结果
  3. //表示退出的管道
  4. exitChan := make(chan bool, 4) //4个
  5. //开启一个协程,向 intChan 放入 1-8000个数
  6. go putNum(intChan)
  7. //开启四个协程, 从 intChan 取出数据,并判断是否为素数,如果是,就放入 primeChan
  8. for i := 0; i < 4; i++ {
  9. go primeNum(intChan, primeChan, exitChan)
  10. }
  11. //这里我们主线程,进行处理
  12. //直接
  13. go func() {
  14. for i := 0; i < 4; i++ {
  15. <-exitChan
  16. }
  17. //当我们从 exitchan 取出 4 个结果,就可以放心关闭 primeNum
  18. close(primeChan)
  19. }()
  20. //遍历我们的 primeNum,把结果取出
  21. for {
  22. result, ok := <- primeChan
  23. if !ok {
  24. break
  25. } else {
  26. //将结果输出
  27. fmt.Printf("素数 = %d \n", result)
  28. }
  29. }
  30. fmt.Println("main() 主线程退出...")

}

  1. 5. 协程求素数代码效率测试
  2. ```go
  3. /main/main.go
  4. package main
  5. import (
  6. "fmt"
  7. "time"
  8. )
  9. // intChan 放入 1-8000个数
  10. func putNum(intChan chan int) {
  11. for i := 0; i < 800000; i++ {
  12. intChan <- i
  13. }
  14. //关闭 intChan
  15. close(intChan)
  16. }
  17. // 从 intChan 取出数据,并判断是否为素数,如果是,就放入 primeChan
  18. func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
  19. // 使用 for 循环
  20. var flag bool
  21. for {
  22. //time.Sleep(time.Microsecond)
  23. num, ok := <- intChan
  24. if !ok { //intChan 取不到
  25. break
  26. }
  27. flag = true //假设是素数
  28. //判断 num 是不是素数
  29. for i := 2; i < num; i++ {
  30. if num % i == 0{
  31. //说明该 num 不是素数
  32. flag = false
  33. break
  34. }
  35. }
  36. if flag {
  37. //将这个数放入到 primeChan
  38. primeChan <- num
  39. }
  40. }
  41. fmt.Println("有一个 primeNum 协程取不到数据,退出")
  42. //这里我们还不能关闭 primeChan
  43. //向 exitChan 写入 true
  44. exitChan <- true
  45. }
  46. func main() {
  47. intChan := make(chan int, 1000)
  48. primeChan := make(chan int, 200000) //放结果
  49. //表示退出的管道
  50. exitChan := make(chan bool, 4) //4个
  51. start := time.Now().Unix()
  52. //开启一个协程,向 intChan 放入 1-8000个数
  53. go putNum(intChan)
  54. //开启四个协程, 从 intChan 取出数据,并判断是否为素数,如果是,就放入 primeChan
  55. for i := 0; i < 4; i++ {
  56. go primeNum(intChan, primeChan, exitChan)
  57. }
  58. //这里我们主线程,进行处理
  59. //直接
  60. go func() {
  61. for i := 0; i < 4; i++ {
  62. <-exitChan
  63. }
  64. end := time.Now().Unix()
  65. fmt.Println("使用协程耗费的时间 =", end - start)
  66. //当我们从 exitChan 取出 4 个结果,就可以放心关闭 primeNum
  67. close(primeChan)
  68. }()
  69. //遍历我们的 primeNum,把结果取出
  70. for {
  71. _, ok := <- primeChan
  72. if !ok {
  73. break
  74. } else {
  75. //将结果输出
  76. //fmt.Printf("素数 = %d \n", result)
  77. }
  78. }
  79. fmt.Println("main() 主线程退出...")
  80. }
  1. 传统求素数方法代码效率测试 ```go /test/test.go package main

import ( “fmt” “time” )

func main() { start := time.Now().Unix() for num := 1; num <= 800000; num++ { flag := true //假设是素数 //判断 num 是不是素数 for i := 2; i < num; i++ { if num % i == 0{ //说明该 num 不是素数 flag = false break } } if flag { //将这个数放入到 primeChan //primeChan <- num } } end := time.Now().Unix() fmt.Println(“普通的方法耗时 = “, end - start) }

  1. 7. 结论
  2. - 使用 go 协程,执行的速度比普通方法提高了至少4倍。(跟 CPU 有关系)
  3. <a name="b85589b6"></a>
  4. #### channel 使用细节和注意事项
  5. 1. channel 可以声明为只读,或者只写性质。
  6. 1. channel 只读和只写的最佳实践案例
  7. ```go
  8. package main
  9. import "fmt"
  10. //ch chan<- int, 这样 ch 就只能写操作
  11. func send(ch chan<- int, exitChan chan struct{}) {
  12. for i := 0; i < 10; i++ {
  13. ch <- i
  14. }
  15. close(ch)
  16. var a struct{}
  17. exitChan <- a
  18. }
  19. //ch <-chan int, 这样 ch 就只能读操作
  20. func recv(ch <-chan int, exitChan chan struct{}) {
  21. for {
  22. value, ok := <- ch
  23. if !ok {
  24. break
  25. } else {
  26. fmt.Println(value)
  27. }
  28. }
  29. var a struct{}
  30. exitChan <- a
  31. }
  32. func main() {
  33. //默认情况下,管道是双向的
  34. var ch chan int
  35. ch = make(chan int, 10)
  36. exitChan := make(chan struct{}, 2)
  37. go send(ch, exitChan)
  38. go recv(ch, exitChan)
  39. var total = 0
  40. for b := range exitChan {
  41. total++
  42. if total == 2 {
  43. break
  44. } else {
  45. fmt.Println(b)
  46. }
  47. }
  48. fmt.Println("结束....")
  49. }
  1. 使用 select 可以解决从管道取数据的阻塞问题。 ```go package main

import ( “fmt” “time” )

func main() { //使用 select 可以解决从管道取数据的阻塞问题

  1. //1. 定义一个管道 10 个数据 int
  2. intChan := make(chan int, 10)
  3. for i := 0; i < 10; i++ {
  4. intChan <- i
  5. }
  6. //2. 定义一个管道 5个数据 string
  7. stringChan := make(chan string, 5)
  8. for i := 0; i < 5; i++ {
  9. stringChan <- "hello" + fmt.Sprintf("%d", i)
  10. }
  11. // 传统的方法在遍历管道时,如果不关闭会阻塞,导致 dead lock!
  12. //在实际开发中,可能不好确定什么时候关闭该管道。
  13. //可以使用 select 方式可以解决
  14. label :
  15. for {
  16. select {
  17. case value := <- intChan ://注意:这里,如果 intchan 一直没有关闭,不会一直 dead lock!,会自动到下个 case 匹配
  18. fmt.Printf("从 intChan 读取的数据 %d \n", value)
  19. time.Sleep(time.Second)
  20. case value := <- stringChan :
  21. fmt.Printf("从 stringChan 读取的数据 %s \n", value)
  22. time.Sleep(time.Second)
  23. default:
  24. fmt.Printf("都取不到,不玩了 \n")
  25. time.Sleep(time.Second)
  26. break label
  27. }
  28. }

}

  1. 4. goroutine 中使用 recover,解决协程中出现 panic,导致程序崩溃问题。
  2. ```go
  3. package main
  4. import (
  5. "fmt"
  6. "time"
  7. )
  8. //函数
  9. func sayHello() {
  10. for i := 0; i < 10; i++ {
  11. time.Sleep(time.Second)
  12. fmt.Println("hello, world!")
  13. }
  14. }
  15. //函数
  16. func test() {
  17. //这里我们可以使用错误处理机制 defer + recover
  18. //定义了一个 map
  19. defer func() {
  20. //捕获 test 抛出的 panic
  21. if err := recover(); err != nil {
  22. fmt.Println("test() 发生错误", err)
  23. }
  24. }()
  25. var myMap map[int]string
  26. myMap[0] = "golang" //error
  27. }
  28. func main() {
  29. go sayHello()
  30. go test()
  31. for i := 0; i < 10; i++ {
  32. fmt.Println("main() ok =", i)
  33. time.Sleep(time.Second)
  34. }
  35. }
  1. 说明:如果我们起了一个协程,但是这个协程出现 panic, 如果我们没有捕获这个 panic,就会造成整个程序的崩溃,这时我们可以在 goroutine 中使用 recover 来捕获 panic,进行处理,这样即使这个协程发生的问题,但是主线程仍然不受影响,可以继续运行。

课程来源