• Go并发编程(上)


    目录

    一、go语言当中的协程

    二、MPG模型介绍

    三、Goroutine  的使用

     3.1  协程的开启

    3.2 优雅地等待子协程结束

    四、捕获子协程的panic

    五、管道Channel

    5.1、认识管道

    5.2、Channel的遍历和关闭

    5.3 、用管道实现生产者消费者模型

    5.4、Channel一些使用细节和注意事项


    一、go语言当中的协程

    在C++中我们要实现并发编程的时候,我们通常需要自己维护一个线程池,并且需要自己去包装一个又一个的任务,同时需要自己去调度线程执行任务并维护上下文切换,这一切通常会耗费程序员大量的心智。那么能不能有一种机制,程序员只需要定义很多个任务,让系统去帮助我们把这些任务分配到CPU上实现并发执行呢?

    Go语言中的goroutine就是这样一种机制,goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制


    Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能–goroutine,当你需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数,开启一个goroutine去执行这个函数就可以了,就是这么简单粗暴

    二、MPG模型介绍

    我们先来看一下Go语言的并发模式,发现是不同于C++的

    下面我们来解释一下MPG模式当中的M、P、G分别代表什么意思

    模型解释:

    G0, G1,G2  谁先执行完是完全不确定的,这不像是java语言,java可以给每个线程分别设置一个优先级,然后控制线程的执行顺序,但是go的话是不行的。  程序员只能把一个协程开启,但是中间的过程是无法去决定的。

    三、Goroutine  的使用

     3.1  协程的开启

    两种方法开启,当然这里只是开启,并没有去等待协程的结束。

    3.2 优雅地等待子协程结束

    父协程结束后,子协程并不会结束。main协程结束后,所有协程都会结束。

    代码演示:

    1. var wg = sync.WaitGroup{}
    2. func Add() {
    3. defer wg.Done()
    4. time.Sleep(1 * time.Second)
    5. fmt.Println("over")
    6. }
    7. func main() {
    8. wg.Add(2)
    9. go Add() //开启了一个协程,并没有等待结束
    10. go Add()
    11. wg.Wait()
    12. }

    四、捕获子协程的panic

    何时会发生panic:

    • 运行时错误会导致panic,比如数组越界、除0。

    • 程序主动调用panic(error)。

    panic会执行什么:

    1. 逆序执行当前goroutine的defer链(recover从这里介入)。

    2. 打印错误信息和调用堆栈。

    3. 调用exit(2)结束整个进程。

    关于defer

    • defer在函数退出前被调用,注意不是在代码的return语句之前执行,因为return语句不是原子操作。

    • 如果发生panic,则之后注册的defer不会执行。

    • defer服从先进后出原则,即一个函数里如果注册了多个defer,则按注册的逆序执行。

    • defer后面可以跟一个匿名函数。

    五、管道Channel

    5.1、认识管道

    管道其本质上是一个环形队列,在这里说明一下定义管道有以下节点需要注意

    1.hannel本质就是一个数据结构-环形队列
    2.数据是先进先出[FIFO : [first in first out]
    3.线程安全,多goroutine访问时,不需要加锁,就是说channel本身就是线程安全的(编译器底部维护的)

    4.channel有类型的,一个string的channel只能存放string类型数据

    下面我们来看看如何定义管道

    1. var intChan chan int //intChan用来存储int数据
    2. var mapChan chan map[int]string//mapChan用来存储map[int]string类型
    3. var perChan chan People//用来存储自定义类型People
    4. var perChan2 chan *People

    在这里有以下几点需要注意

    • 管道channel 是引用类型,需要初始化以后才能插入数据,也就是make
    • 管道是有类型的,管道的类型是什么就只能写入这种类型的数据
    • 当管道写满了以后,在没有别的协程的情况下,再次写入会导致死锁
    • 在没有使用协程的情况下(取完没放入),当管道为空,再取,会报deadlock
    • 遍历管道时需要提前把管道关闭(close),否则会导致死锁

    代码进行演示

    1. func main() {
    2. ch := make(chan int, 5)
    3. //创建一个管道
    4. for i := 0; i < 5; i++ {
    5. ch <- i //在管道当中写入数据
    6. }
    7. for len(ch) > 0 {
    8. value := <-ch
    9. fmt.Println(value)
    10. }
    11. ch = make(chan int) //非缓冲通道
    12. val := <-ch //没有初始化就取数据,会发生错误的
    13. fmt.Println(val)
    14. //注意channel关闭之后不能向channel当中写入数据,否则会造成死锁
    15. /*
    16. channel支持for --range遍历但是请注意两个细节
    17. 遍历时如果channel没有关闭则会出现deadlock的错误
    18. 在遍历时如果channel以及关闭了则会正常的遍历数据,遍历完毕之后就会退出吧遍历
    19. */
    20. intchan := make(chan int, 100)
    21. for i := 0; i < 100; i++ {
    22. intchan <- i
    23. //放入100个数据到channel当中
    24. }
    25. //for i := 0; i < len(intchan); i++ {
    26. // fmt.Println(<-intchan)
    27. // //注意这样会少50个数据所以不能这样遍历管道的长度是一直在变的
    28. //}
    29. close(intchan) //遍历其一定需要将管道关闭否则会造成死锁
    30. for v := range intchan {
    31. fmt.Println(v)
    32. }
    33. }

    5.2、Channel的遍历和关闭

    1.channel的关闭:使用内置函数close可以关闭channel,当channel关闭后,就不能再向channel写数据了,但是仍然可以从该channel读取数据,就算读取数据个数大于容量,也能读取到,只是读出来的是0 ,这时候就需要用  v,ok := < - intChan,  如果管道没有关闭,是会阻塞在这一步的,也就是说ok这里不会有值

    1. for {
    2. v, ok := <-intChan2
    3. if !ok {//证明没有数据了
    4. fmt.Println("没有数据了")
    5. fmt.Println(v) // 读出来的是0
    6. break
    7. }
    8. fmt.Println(v)
    9. }

    2.channel支持for-range 遍历和普通for进行遍历,但是普通的for循环遍历, 因为取出操作本身会导致长度变化所以我们不建议使用。

    3.在遍历时,如果channel没有关闭,则回出现deadlock的错误。在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
     

    1. func main() {
    2. intChan := make(chan int, 3)
    3. intChan <- 1
    4. intChan <- 2
    5. close(intChan) //关闭管道
    6. //关闭管道后就不能存放了,但是可以取数据
    7. x1 := <-intChan
    8. x2 := <-intChan
    9. //x3 := <-intChan
    10. //x4 := <-intChan
    11. fmt.Println(x1)
    12. fmt.Println(x2)
    13. //fmt.Println(x3)
    14. //fmt.Println(x4)
    15. intChan2 := make(chan int, 100)
    16. for i := 0; i < 100; i++ {
    17. intChan2 <- i
    18. }
    19. //遍历管道前切记要先close
    20. //不能使用普通的for循环,因为管道的数量在动态变化的 ,但是这里如果提前直到数量是100,循环的话i<100 就行
    21. //for i := 0; i < len(intChan2); i++ {
    22. // x := <-intChan2
    23. // fmt.Println(x)
    24. //}
    25. close(intChan2) //遍历前切记要先关闭管道
    26. for v := range intChan2 {
    27. fmt.Println(v)
    28. }
    29. }

    5.3 、用管道实现生产者消费者模型

    1.开启一个Writea协程,向管道intChan中写入50个整数
    2.开启一个Read协程,从管道intChan中读取writeData写入的数据。

    3.注意: Write和Read操作的是同一个管道
    4.主线程需要等待Write和Read协程都完成工作才能退出[管道]

    代码展示:

    1. package main
    2. import (
    3. "fmt"
    4. )
    5. func Write(intChan chan int) {
    6. for i := 0; i < 50; i++ {
    7. fmt.Printf("写入数据 :%d \n", i)
    8. intChan <- i
    9. }
    10. close(intChan) //写完之后关闭管道,但是不影响取
    11. }
    12. func Read(intChan chan int, boolChan chan bool) {
    13. for {
    14. //time.Sleep(time.Millisecond * 100) //先等一秒,为了留时间给生产者,不等可能会直接退出
    15. v, ok := <-intChan
    16. if !ok { //证明已经没有数据了
    17. fmt.Println("管道已经没有数据了。。。")
    18. break
    19. }
    20. fmt.Printf("读出了数据 :%d\n", v)
    21. }
    22. //任务完成向管道当中写入数据
    23. boolChan <- true
    24. //通知主线程该结束了
    25. close(boolChan)
    26. }
    27. func main() {
    28. intChan := make(chan int, 10)
    29. boolChan := make(chan bool, 1) //用来标记是否执行完的管道
    30. go Read(intChan, boolChan) //开启消费者
    31. go Write(intChan) //开启生产者
    32. for { //让主线程一直在这里等待,直到boolChan有数据了
    33. _, ok := <-boolChan
    34. if ok {
    35. break
    36. }
    37. }
    38. }

    5.4、Channel一些使用细节和注意事项

    在go语言当中如果某个协程出现了异常,如果我们不做任何处理那么就会导致整个程序崩溃掉。在go语言当中我们可以使用   

         defer + recover来处理整个异常。

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

    1. package main
    2. import (
    3. "fmt"
    4. "time"
    5. )
    6. /*
    7. go语言当中使用recover解决协程当中出现的panic导致程序崩溃的问题
    8. 如果一个协程出现了异常会导致整个程序崩溃,此时我们需要使用recover来捕获这个panic这样就不会影响其它协程
    9. */
    10. func Say() {
    11. for i := 0; i < 10; i++ {
    12. fmt.Println("hello world")
    13. }
    14. }
    15. func Test() {
    16. //使用defer + recover捕获抛出的panic
    17. defer func() {
    18. if err := recover(); err != nil {
    19. fmt.Println("test()协程发生错误:\n", err)
    20. }
    21. }()
    22. var myMap map[int]string //需要提前make
    23. myMap[0] = "提升和" //没有提前make
    24. }
    25. func main() {
    26. go Test()
    27. go Say()
    28. time.Sleep(time.Second)
    29. }
  • 相关阅读:
    面试-SpringCloud常见组件和注册表结构+nacos
    给你两个集合,要求{A} + {B}
    【跨境电商】全渠道客户服务终极指南(一):概念,重要性与优势
    二叉苹果树(树形DP>>有依赖的背包问题)
    c++ - 第9节 - vector类
    幼儿园管理系统|基于springboot框架+ Mysql+Java+Tomcat的幼儿园管理系统设计与实现(可运行源码+数据库+设计文档+部署说明)
    kimera论文阅读
    【设计模式】Head First 设计模式——工厂方法模式 C++实现
    达梦DBLINK之DM访问Oracle问题处理
    竞赛 大数据商城人流数据分析与可视化 - python 大数据分析
  • 原文地址:https://blog.csdn.net/flyingcloud6/article/details/133499365