go并发编程


预备知识

进程和线程

image.pngimage.png

并发和并行

image.png
image.png
image.png

golang设置CPU

image.png


协程 - goroutine

  • 一个Go主线程(也可以理解为进程)上可以起很多的协程
  • 协程是轻量级线程,底层根据线程特点进行了大量优化[编译器底层优化]
  • 有独立的栈空间
  • 共享程序堆空间
  • 调度由用户控制
  • 协程是轻量级的线程

MPG模式

image.png
image.png
image.png


管道 - channel

基本使用

  1. //声明管道
  2. var intChan chan int //intChan用于存放int数据
  3. //创建一个可以存放3个int类型的管道
  4. intChan = make(chan int, 3)
  5. //向管道写入数据
  6. num := 211
  7. intChan<- 100
  8. intChan<- num
  9. //从管道中读取数据
  10. var num2 int
  11. num2 = <-intChan
  12. fmt.Println(num2)
  13. fmt.Printf("channel len=%v cap=%v", len(intChan), cap(intChan))

channel的遍历和关闭

image.png

  1. //管道的遍历 - for range
  2. intChan2 := make(chan int, 100)
  3. for i := 0; i < 100; i++{
  4. intChan2<- i
  5. }
  6. close(intChan2) //关闭管道
  7. //没close 死锁错误
  8. for v := range intChan2{
  9. fmt.Println("v = ", v)
  10. }

image.png

并发安全问题

并发写问题

  1. 多个协程同时修改同一全局变量
  1. //全局变量
  2. var (
  3. myMap = make(map[int]int, 10)
  4. )
  5. // 使用goroutine计算 1-200 各个数的阶乘,并把各个数的阶乘放到 map 中
  6. func Demo3() {
  7. //启用200个协程
  8. for i := 1; i <= 200; i++{
  9. go channelTest(i)
  10. }
  11. time.Sleep(time.Second * 10)
  12. for key, value := range myMap {
  13. fmt.Printf("%v! = %v", key, value)
  14. }
  15. }
  16. // 计算n的阶乘,将结果放入到map中
  17. func channelTest(n int){
  18. res := 1
  19. for i := 1; i<=n ; i++{
  20. res *= i
  21. }
  22. myMap[n] = res
  23. }

image.png
使用 go build -race 编译可检查是否有数据冲突(data race)

使用互斥锁(初级)

//全局变量 lock sync.Mutex

  1. //使用互斥锁
  2. lock.Lock()
  3. myMap[n] = res
  4. lock.Unlock()
  1. var (
  2. //全局变量
  3. myMap = make(map[int]int, 10)
  4. //全局变量 互斥锁
  5. lock sync.Mutex
  6. )
  7. // 使用goroutine计算 1-200 各个数的阶乘,并把各个数的阶乘放到 map 中
  8. func Demo3() {
  9. //启用200个协程
  10. for i := 1; i <= 200; i++{
  11. go channelTest(i)
  12. }
  13. time.Sleep(time.Second )
  14. for key, value := range myMap {
  15. fmt.Printf("%v! = %v\n", key, value)
  16. }
  17. }
  18. // 计算n的阶乘,将结果放入到map中
  19. func channelTest(n int){
  20. res := 1
  21. for i := 1; i<=n ; i++{
  22. res *= i
  23. }
  24. //使用互斥锁
  25. lock.Lock()
  26. myMap[n] = res
  27. lock.Unlock()
  28. }

协程结合管道

image.png

  1. package goroutineDemo
  2. import (
  3. "fmt"
  4. )
  5. // 一个管道,两个协程
  6. // 一个读,一个写
  7. // 主线程需要等待协程操作完成再关闭
  8. func Channel1() {
  9. // 创建两个管道
  10. intChan := make(chan int, 50) //数据管道
  11. exitChan := make(chan bool, 1) //退出管道
  12. go writeData(intChan)
  13. go readData(intChan, exitChan)
  14. //time.Sleep(time.Second * 10)
  15. //等待协程
  16. for{
  17. _ , ok := <-exitChan
  18. if !ok{
  19. break
  20. }
  21. }
  22. }
  23. func writeData(intChan chan int){
  24. for i:=1; i<=50; i++{
  25. intChan<- i
  26. fmt.Println("写入数据 ",i)
  27. }
  28. close(intChan)
  29. }
  30. func readData(intChan chan int,exitChan chan bool){
  31. for {
  32. v,ok := <-intChan
  33. if !ok {
  34. break
  35. }
  36. fmt.Printf("readData %v\n",v)
  37. }
  38. //告知主线程
  39. exitChan<- true
  40. close(exitChan)
  41. }

image.png

注意事项

  • channel中只能存放指定的数据类型
  • channel的数据放满后再放,会报死锁错误
  • 单线程 - channel没有close的情况下:channel中的数据为空再取,会报死锁错误
  • 单线程 - channel已经close的情况下:channel中的数据为空再取,v,ok = <-channel 中 ok会被置为false
  • 多线程 - 多线程情况下,同一管道如果只读不写或者只写不读,编译器会报错 deadlock!
  • 多线程 - 多线程情况下,同一管道如果读写速率不一致,则速率快的会有效阻塞
  • 只读管道 var intChan <-chan int ; 只写管道 :var intChan chan<- int

只读/只写管道可以在函数传参时限制函数内的操作方式
image.png

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

    1. //使用select解决从管道取数据的阻塞问题
    2. //容量为10的int管道
    3. intChan := make(chan int,10)
    4. for i := 0; i < 10; i++ {
    5. intChan<- i
    6. }
    7. //容量为5的string管道
    8. stringChan := make(chan string, 5)
    9. for i := 0; i < 5; i++ {
    10. stringChan<- "hello "+fmt.Sprintf("%d",i)
    11. }
    12. //传统方法遍历时,如果没有close管道,会阻塞导致死锁
    13. //for {
    14. // v := <-intChan
    15. // fmt.Println(v)
    16. //}
    17. for {
    18. select {
    19. //此种方式,即使管道一直不关闭,也不会一直阻塞导致deadlock
    20. //会自动到下一个case匹配
    21. case v := <-intChan :
    22. fmt.Printf("从intChan中读取数据%d\n",v)
    23. case v := <-stringChan :
    24. fmt.Printf("从stringChan中读取数据%s\n",v)
    25. default:
    26. fmt.Println("都取不到了,不玩了!!!")
    27. return
    28. }
    29. }

    image.png

  • goroutine中使用defer+recover,可以解决单个协程出错影响其他协程以及主线程的问题 ```go // 一个协程报错导致程序崩溃 func RecoverDemo() { go sayHello() go errorFunc()

    time.Sleep(time.Second * 5) }

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

func errorFunc(){ var myMap map[int]string //没有make,直接赋值 myMap[0] = “golang” }

  1. ```go
  2. // defer + recover
  3. func errorFunc(){
  4. defer func() {
  5. //捕获errorFunc抛出的panic
  6. if err := recover(); err!=nil{
  7. fmt.Println("函数错误!!",err)
  8. }
  9. }()
  10. var myMap map[int]string
  11. //没有make,直接赋值
  12. myMap[0] = "golang"
  13. }