• GO语言之Goroutine和channel


    1,goroutine-看一个需求

    需求:要求统计1-90000000000的数字中,哪些是素数哦?

    分析思路:

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

    2)使用并发或者并行的方式,将统计素数的任务分配给多个goroutine去完成,这时就会用到goroutine

    2,goroutine-基本介绍

    2.1进程和线程介绍

    2.2程序,进程和线程的关系示意图

    2.3并发和执行

    并发和并行

    1)多线程程序在单核上运行就是并发

    2)多线程程序在多核上运行就是并行

    3)图示:

    并发:一个cpu同时执行多个线程

    并行:多个cpu上执行多个线程,就相当于一个cpu执行一个线程

    2.4GO协程核GO主线程

    GO主线程(有程序员直接称为线程/也可以理解为进程):一个GO线程上,可以有多个协程,你可以理解为协程是轻量级的线程[编译器做的优化]

    GO协程的特点

    1)有独立的栈空间

    2)共享程序堆空间

    3)调度由用户控制

    4)协程是轻量级的线程

    3,goroutine-简单例子

    3.1案例说明

    请编写一个程序,完成如下功能

    1)在主线程(可以理解成进程)中,开启一个goroutine,该协程每隔一秒输出一次helloworld“

    2)在主线程中也每隔一秒输出”hellogolang“,输出10次后,退出程序

    3)要求主线程和goroutine同时执行

    4)画出主线程和协程的执行流程图

    主线程和协程执行流程图

    3.2小结

    1)主线程是一个物理线程,直接作用在cpu上。是重量级的,非常耗费cpu资源

    2)协程从主线程开启,是轻量级的线程,是逻辑态。对资源的耗费相对较小

    3)golang的协程机制是重要的特点,可以轻松的开启上万个协程。其他语言的并发机制是一般基于线程的,开启过多的线程耗费资源大,这里就凸显出golang的优势了

    4,goroutine的调度模型

    4.1MPG模式基本介绍

    1)M:操作系统的主线程(是物理线程)

    2)P:协程执行需要的上下文

    3)G:协程

    4.2MPG模式运行的状态1

    1)当前程序有三个M,如果三个M都在一个CPU运行,就是并发,如果在不同的CPU下运行就并行

    2)M1,M2,M3正在执行一个G,M1的协程队列有三个,M2的协程队列有三个,M3的协程队列有两个

    3)从上图可以看出:GO的协程是轻量级的线程,是逻辑态的,GO可以容易起上万个协程

    4)其他程序c/java的多线程往往是内核态的,比较重量级,几千个线程可能耗光CPU

    4.3MPG模式运行的状态2

    1)分成两部分来看

    2)原来的情况是M0主线程正在执行G0线程,另外有三个线程在队列等待

    3)如果G0线程阻塞,比如读取文件或者数据库等

    4)这时就会创建出m1主线程(也可能是从已经有的线程中取出M1)并且将等待的3个协程挂到M1下开始执行,M0的主线程下的G0仍然执行io的读写

    5)这样的MPG调度模式,可以既让G0执行同时也可以让队列的其他的协程执行,仍然可以并发执行

    6)等到G0不阻塞了,M0会被放到空闲的主线程继续执行(从已有的线程池中获取),同时G0又会被唤醒

    5,设置Golang运行的CPU数

    说明:为了充分利用多cpu的优势,在Goalng程序中,设置cpu数目

    6,channel(管道)-需求

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

    思路:

    1)使用goroutine来完成,效率高,但是会出现并发/并行安全问题

    2)这里就提出了不同goroutine如何通信的问题

    代码实现:

    1)使用goroutine来完成(看看goroutine并发完成会出现什么问题)

    2)在运行某个程序时,如何知道是否存在资源竞争问题。在编译程序时,增加一个参数-race即可

    1. package Goroutine
    2. import (
    3. "fmt"
    4. "time"
    5. )
    6. var(
    7. myMap=make(map[int]int,10)
    8. )
    9. //计算n!并放入到map里
    10. func operation(n int) {
    11. res:=1
    12. for i:=1;i<=n;i++{
    13. res*=i
    14. }
    15. myMap[n]=res
    16. }
    17. func Test3() {
    18. //我们开启多个协程去完成这个任务
    19. for i:=1;i<=200;i++{
    20. go operation(i)
    21. }
    22. time.Sleep(time.Second*10)
    23. fmt.Println(myMap)
    24. }

    会有错误产生

    6.1不同goroutine之间如何通讯

    1)全局变量互斥锁

    2)使用channel来解决

    6.2使用全局变量加锁同步改进程序

    因为没有对全局变量加锁,因此会出现资源争夺的问题,代码会出现错误,提示

    解决方案:加入互斥锁

    我们的数阶乘很大,将数改为uint64()

    1. package Goroutine
    2. import (
    3. "fmt"
    4. "sync"
    5. "time"
    6. )
    7. var(
    8. myMap2=make(map[int]uint64,10)
    9. //声明一个全局互斥锁,lock是一个全局互斥锁,sync是包:synchorized同步
    10. //Mutex:是互斥
    11. lock sync.Mutex
    12. )
    13. func operation2(n int) {
    14. var res uint64=1
    15. for i:=1;i<=n;i++{
    16. res*=uint64(i)
    17. }
    18. //我们将res放入myMap
    19. //加锁
    20. lock.Lock()
    21. myMap2[n]= res
    22. //解锁
    23. lock.Unlock()
    24. }
    25. func Test4() {
    26. for i:=1;i<=200;i++{
    27. go operation2(i)
    28. }
    29. time.Sleep(time.Second*10)
    30. //这里我们输出
    31. //加锁
    32. lock.Lock()
    33. fmt.Println(myMap2)
    34. lock.Unlock()
    35. }

    6.3为什么需要channel

    1)前面使用全局变量加锁同步来解决goroutine的通讯并不完美

    2)主线程在等待所有goroutine全部完成的时间很难确定,我们这里设置10秒,仅仅是估算

    3)如果主线程休眠时间长了,会加长等待时间,如果时间短了,可能还有goroutine处于工作状态,这时也会随着主线程的退出而销毁

    4)通过全局变量加锁同步来实现通讯,也并不利于多个协程对全局变量进行读写操作

    5)下面来讲以下channel

    6.4 channel的基本介绍

    1)channel本质是一个数据结构+队列

    2)数据是先进先出

    3)线程安全,多个goroutine访问时,不需要加锁,就是说channel本身就是线程安全的

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

    6.5定义/声明管道

    var 变量名 chan 数据类型

    举例:

    var intChan chan int 存放int类型

    var mapChan chan map[int]string mapChan存放map[int]string类型

    var perChan chan Person...

    说明

    channel是引用类型

    channel必须初始化才能写入数据,即make后才能用

    管道是有类型的,intChan只能写入整数int

    6.6管道的初始化,写入数据到管道,从管道读取数据,注意事项

    1. package Goroutine
    2. import "fmt"
    3. func Test5() {
    4. //演示一下管道的使用
    5. //1,创建一个可以存放3个int类型的管道
    6. var intChan chan int
    7. intChan=make(chan int,3)
    8. //2,看看intChan是什么
    9. fmt.Printf("值:%V 地址:%p\n",intChan,&intChan)
    10. //3,向管道写入数据
    11. intChan<-10
    12. num:=211
    13. intChan<-num
    14. intChan<-50
    15. //intchan<-99注意不要超过它的容量
    16. //4,看看管道的长度和cap(容量)
    17. fmt.Printf("长度 len=%v cap=%v\n",len(intChan),cap(intChan))
    18. //5,从管道里读取数据
    19. var num2 int
    20. num2=<-intChan
    21. fmt.Println("num2=",num2)
    22. fmt.Printf("长度 len=%v cap=%v\n",len(intChan),cap(intChan))
    23. //6,在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报告deadlock
    24. num3:=<-intChan
    25. num4:=<-intChan
    26. num5:=<-intChan
    27. fmt.Println("num3=",num3,"num4=",num4,"num5=",num5)
    28. }

    6.7channel使用的注意事项

    1)channel只能存放指定的数据类型

    2)channel的数据放满后,就不能再放入了

    3)如果从channel取出数据后就可以继续放入

    4)在没有使用协程的情况下,如果channel数据取完了,再取就会报错

    6.8读写channel案例演示

    1)创建一个intChan,最多可以放3个int,演示存三个数据到intChan然后再取出这三个int

    2)创建一个mapChan,最多可以存放1个map[string]string演示写入和读取

    3)创建一个结构体变量cat,创建一个管道,演示catChan的存取

    4)创建一个allchan可以存放任意数据类型的变量

    7,练习

    1)创建一个person结构体[Name,Age,Address]

    2)创建10个Person实例,并放入到channel中

    3)遍历channel,将各个person实例信息显示在终端

    1. package Goroutine
    2. import "fmt"
    3. type Person struct {
    4. name string
    5. Age int
    6. Address string
    7. }
    8. func Test9() {
    9. var PersonChan chan Person
    10. PersonChan=make(chan Person,10)
    11. for i:=0;i<10;i++{
    12. PersonChan<-Person{"wang",i,"asa"}
    13. }
    14. for len(PersonChan)!=0{
    15. fmt.Println(<-PersonChan)
    16. }
    17. }

    8,chaneel的遍历和关闭

    8.1channe的关闭

    使用内置函数close可以关闭channel,当channel关闭后,就不能再向channel写数据了,但是仍然可以从该channel读取数据

    8.2channel的遍历

    channel支持for-range的方式进行遍历

    1)在遍历时,如果channel没有关闭,则会出现deadlock错误

    2)在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后就会退出遍历

    8.3channel遍历和关闭的案例演示

    8.4应用实例1

    请完成goroutine和channel协同工作的案例

    1)开启一个writeData协程,向管道intChan中写入五十个数

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

    3)注意:WriteData和readData操作的是同一个管道

    4)主线程需要等待writeData和readData协程都完成工作才能退出管道

    1. package Goroutine
    2. import (
    3. "fmt"
    4. )
    5. //writeData
    6. func writeData(intChan chan int) {
    7. for i:=1;i<=50;i++{
    8. intChan<-i
    9. fmt.Println("writeData",i)
    10. }
    11. close(intChan)
    12. }
    13. //readData
    14. func readData(intChan chan int,exitChan chan bool) {
    15. for{
    16. v,ok:=<-intChan
    17. if !ok{
    18. break
    19. }
    20. fmt.Println("readData",v)
    21. //time.Sleep(time.Second)
    22. }
    23. exitChan<-true
    24. close(exitChan)
    25. }
    26. func Test12() {
    27. //创建两个管道
    28. intChan:=make(chan int,50)
    29. exitChan:=make(chan bool,1)
    30. go writeData(intChan)
    31. //time.Sleep(time.Second)
    32. go readData(intChan,exitChan)
    33. //time.Sleep(time.Second)
    34. for{
    35. _,ok:=<-exitChan
    36. fmt.Println("sa")
    37. if!ok{
    38. break
    39. }
    40. }
    41. }

    8.5应用实例2-阻塞

    如果只是向管道写入数据,而没有读取,就会出现阻塞而dead lock,原因是intChan容量是10,而代码writeData会写入50个数据,因此会阻塞在writeData的ch<-i

    8.6应用实例3

    要求统计1-200000的数字中,哪些是素数?我们这里采用goroutine和channnel来完成,测试数据是80000

    分析思路:

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

    现在使用并发知识,将统计素数的任务分配给4个goroutine去完成

    1. package Goroutine
    2. import (
    3. "fmt"
    4. "time"
    5. )
    6. //向intChan放入1-8000个数字
    7. func putNum(intChan chan int){
    8. for i:=1;i<=8000;i++{
    9. intChan<-i
    10. }
    11. //关闭intChan
    12. close(intChan)
    13. }
    14. //从intChan中取出数据,并判断是不是素数,如果是就放入到primeChan
    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. num,ok:=<-intChan
    20. if!ok{//intChan取不到
    21. break
    22. }
    23. flag=true
    24. for i:=2;i<num;i++{
    25. if num%i==0{
    26. flag=false
    27. break
    28. }
    29. }
    30. if flag{
    31. //就将这个数放入到primeChan
    32. primeChan<-num
    33. }
    34. }
    35. fmt.Println("有一个primeNUM因为取不到数据,退出")
    36. //向exitChan写入true
    37. exitChan<-true
    38. }
    39. func Test13() {
    40. intChan:=make(chan int,1000)
    41. primeChan:=make(chan int,2000)//放入结果
    42. //标识退出管道
    43. exitChan:=make(chan bool,4)
    44. go putNum(intChan)
    45. //开启四个协程
    46. for i:=0;i<4;i++{
    47. go primeNum(intChan,primeChan,exitChan)
    48. }
    49. //这里我们主线程进行处理
    50. //直接
    51. go func() {
    52. for i:=0;i<4;i++{
    53. fmt.Println("exitchan",<-exitChan)
    54. }
    55. //当我们从exitChan中取出来四个结果后,就可以放心的关闭prprimeChan
    56. close(primeChan)
    57. }()
    58. //遍历我们的primeChan
    59. for{
    60. res,ok:=<-primeChan
    61. if !ok{
    62. break
    63. }
    64. fmt.Println("素数",res)
    65. }
    66. fmt.Println("main线程退出")
    67. }

    9,channel使用细节和注意事项

    1)channel可以声明为只读,或者只写性质

    2)channel只读和只写的最佳实践案例

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

    1. package Goroutine
    2. import (
    3. "fmt"
    4. "time"
    5. )
    6. func Test15() {
    7. //1,定义一个管道存10个数据
    8. intChan:=make(chan int,10)
    9. for i:=0;i<10;i++{
    10. intChan<-i
    11. }
    12. //2,定义一个管道存5个数据string
    13. stringChan:=make(chan string,5)
    14. for i:=0;i<5;i++ {
    15. stringChan<-"sww"+fmt.Sprintf("%d",i)
    16. }
    17. //在传统的方法中我们不关闭会阻塞而导致deadlock
    18. //我们可以使用select方式解决
    19. for {
    20. select{
    21. //注意,这里如果intchan一直没有关闭,不会一直阻塞而deadlock,会自动到下一个case匹配
    22. case v:=<-intChan:
    23. fmt.Println("数据",v)
    24. time.Sleep(time.Second)
    25. case v:=<-stringChan:
    26. fmt.Println("字符串",v)
    27. time.Sleep(time.Second)
    28. default:
    29. fmt.Println("都取不到了")
    30. return
    31. }
    32. }
    33. }

    4)goroutine中使用recover解决协程中出现的panic,导致程序出现问题,这时我们可以在goroutine中使用recover来捕获panic进行处理,这样即使这个协程发生问题,但是主线程仍然不受影响,可以继续执行

    1. package Goroutine
    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 world")
    10. }
    11. }
    12. //函数
    13. func errorrecover(){
    14. //这里我们可以使用defer+recover
    15. defer func() {
    16. //捕获test抛出的panic
    17. err:=recover()
    18. if err!=nil{
    19. fmt.Println("test()发生错误",err)
    20. }
    21. }()
    22. var m1 map[int]string
    23. m1[0]="ss"
    24. }
    25. func Test16() {
    26. go sayHello()
    27. go errorrecover()
    28. for i:=0;i<10;i++{
    29. fmt.Println(i)
    30. time.Sleep(time.Second)
    31. }
    32. }


    码字不易,还望点个赞,点个关注多多支持一下!谢谢!

    (免费订阅,永久学习)学习地址: Dpdk/网络协议栈/vpp/OvS/DDos/NFV/虚拟化/高性能专家-学习视频教程-腾讯课堂

    更多DPDK相关学习资料有需要的可以自行报名学习,免费订阅,永久学习,或点击这里加qun免费
    领取,关注我持续更新哦! !  

    原文链接;https://zhuanlan.zhihu.com/p/419186816 

  • 相关阅读:
    Prometheus 监控告警系统搭建(对接飞书告警)
    Ajax基础概念和接口及Axios语法和FormData
    【瑞萨零基础入门】瑞萨MCU零基础入门系列教程(更新连载中)
    win10应用商店怎么重新安装?
    【web-攻击后端组件】(7.2)操作文件路径、注入XML解释器、注入后端HTTP请求、注入电子邮件
    基于微信小程序的药店管理系统设计与实现-计算机毕业设计源码+LW文档
    擎创技术流 | Prometheus与Zabbix的融合实践
    软件杯 深度学习YOLO图像视频足球和人体检测 - python opencv
    计算机毕业设计 高校课程评价系统的设计与实现 Java实战项目 附源码+文档+视频讲解
    Linux tips: shell中启动多进程并行批处理的技巧
  • 原文地址:https://blog.csdn.net/lingshengxiyou/article/details/127965596