设为首页收藏本站
网站公告 | 这是第一条公告
     

 找回密码
 立即注册
缓存时间23 现在时间23 缓存数据 荣耀也罢,屈辱也罢,都要以平和的心态去面对,少一些无奈与感慨,多一份从容和淡然。晚安!

荣耀也罢,屈辱也罢,都要以平和的心态去面对,少一些无奈与感慨,多一份从容和淡然。晚安!

查看: 1269|回复: 1

Go 并发编程Goroutine的实现示例

[复制链接]

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
31
主题
25
精华
0
金钱
93
积分
60
注册时间
2023-10-3
最后登录
2025-3-14

发表于 2025-2-16 21:24:13 | 显示全部楼层 |阅读模式
目录


  • 一、主Goroutine

    • 封装main函数的Goroutine被称为主Goroutine
    • 此后主Goroutine会进行一系列的初始化工作:

  • 二、Goroutine

    • GO中使用Goroutine来实现并发
    • 在GO语言中使用goroutine,在调用函数或者方法前面加上go关键字即可  

  • 三、runtime
  • 四、互斥锁

    • 使用sync包下的锁解决临界资源安全问题(Mutex)
    • sync包下的同步等待组(WaitGroup)

  • 五、Channel通道

    • 不要以共享内存的方式通信,而要以通信的方式共享内存
    • 关闭通道
    • 缓冲通道

  • 定向通道

    • Select
    • 利用通道解决临界资源安全问题

进程(Process),线程(Thread),协程(Goroutine,也叫轻量级线程)
进程
进程是一个程序在一个数据集中的一次动态执行过程,进程一般由程序,数据集,进程控制块三部分组成
线程
线程也叫轻量级进程,他是一个基本的CPU执行单元,也就是程序执行过程中的最小单元,由线程ID,程序计数器,寄存器集合和堆栈共同组成的,一个进程可以包含多个线程
协程
协程是一种用户态的轻量级线程,又称微线程,协程的调度完全由用户控制

一、主Goroutine


封装main函数的Goroutine被称为主Goroutine

主Goroutine所做的事情并不是执行main函数那么简单,它首先要做的是设定每一个goroutine所能申请的栈空间的最大尺寸,在32位计算机系统中此最大尺寸为250MB,而在64位计算机系统中此尺寸为1GB,如果有某个Goroutine的栈空间尺寸大于这个限制,那么运行时系统就会引发一个栈溢出(stack overflow)的运行时恐慌,随后这个go程序的运行也会终止

此后主Goroutine会进行一系列的初始化工作:

1、创建一个特殊的defer语句,用于在主Goroutine退出时做必要的善后处理,因为主Goroutine也可能非正常结束
2、启动专用于在后台清扫内存垃圾的Goroutine,并设置GC可用的表示
3、执行main包中所引用包的init函数
4、执行main函数

二、Goroutine


GO中使用Goroutine来实现并发

Goroutine是与其他函数或方法同时运行的函数或方法,与线程相比创建goroutine的成本很小,他就是一段代码,一个函数入口,以及在堆上为其分配一个堆栈(初始大小为4k,会随着程序的执行自动增长删除)。

在GO语言中使用goroutine,在调用函数或者方法前面加上go关键字即可  
  1. package main

  2. import "fmt"

  3. func main() {

  4.         // 使用go关键字使用goroutine调用hello函数
  5.         go hello()
  6.         for i := 0; i < 150000; i++ {
  7.                 //fmt.Println("main-", i)
  8.         }
  9. }

  10. func hello() {
  11.         for i := 0; i < 10; i++ {
  12.                 fmt.Println("hello-----------", i)
  13.         }
  14. }

  15. /*
  16.     此处代码可设置main协程for循环次数的大小观测go协程调用hello情况
  17. */
复制代码

  • 当新的Goroutine开始时,Goroutine调用立即返回,与函数不同,go不等待Goroutine执行结束
  • 当Goroutine调用,并且Goroutine的任何返回值被忽略之后,go立即执行到下一行代码
  • mian的Goroutine应该为其他的Goroutine执行,如果main的Goroutine终止了,程序将被终止,而其他的Goroutine将不会运行

三、runtime


  • 获取系统信息
  • schedule调度让出时间片,让别的goroutine先执行
  • Goexit   //终止当前的goroutine
Go 语言的
  1. runtime
复制代码
包提供了与 Go 运行时环境交互的各种功能。这个包允许你控制和检查程序的运行时行为,包括但不限于:

  • 垃圾回收(Garbage Collection):可以手动触发垃圾回收,或者调整垃圾回收的策略。
  • 并发控制:提供了包括
    1. Gosched()
    复制代码
    在内的方法来控制 goroutine 的调度。
  • 程序退出:可以正常或非正常地退出程序。
  • 堆栈管理:可以获取当前 goroutine 的堆栈信息。
  • 环境变量:读取和设置环境变量。
  • 系统信号:处理操作系统信号。
  • CPU 信息:获取 CPU 的数量和相关信息。
  • 内存分配:可以手动分配和释放内存。
  • 性能监控:可以监控程序的 CPU 使用情况。
以下是一些
  1. runtime
复制代码
包中常用函数的简要说明:

    1. runtime.GOMAXPROCS
    复制代码
    :设置最大可运行的操作系统线程数。
    1. runtime.NumCPU
    复制代码
    :返回机器的 CPU 核心数。
    1. runtime.NumGoroutine
    复制代码
    :返回当前运行的 goroutine 数量。
    1. runtime.Gosched
    复制代码
    :让出 CPU 时间片,使得其他 goroutine 可以运行。
    1. runtime.Goexit
    复制代码
    :退出当前的 goroutine。
    1. runtime.KeepAlive
    复制代码
    :确保某个 goroutine 不会被垃圾回收。
    1. runtime.SetFinalizer
    复制代码
    :为对象设置终结器,当垃圾回收器准备回收该对象时,会调用该终结器。
    1. runtime.GC
    复制代码
    :强制运行垃圾回收器。
  1. package main

  2. import (
  3.         "fmt"
  4.         "runtime"
  5. )

  6. func main() {

  7.         //获取系统信息
  8.         fmt.Println("获取GOROOT目录", runtime.GOROOT())
  9.         fmt.Println("获取操作系统", runtime.GOOS)
  10.         fmt.Println("获取CPU", runtime.NumCPU())

  11.         //Goroutine 调度
  12.         go func() {
  13.                 for i := 0; i < 100; i++ {
  14.                         fmt.Println("Goroutine---", i)
  15.                 }
  16.         }()

  17.         for i := 0; i < 100; i++ {
  18.                 //让出时间片,让别的Goroutine先执行,不一定可以让成功
  19.                 runtime.Gosched()
  20.                 fmt.Println("main---", i)
  21.         }
  22. }
复制代码
  1. runtime.Gosched()
复制代码
是 Go 语言运行时库中的一个函数,它用于让出 CPU 时间片,让其他 goroutine(轻量级线程)有机会执行。这通常用于避免阻塞或减少阻塞的持续时间,尤其是在长时间运行的 goroutine 中,你可能会在适当的地方调用
  1. Gosched
复制代码
来让出 CPU,以避免长时间占用 CPU 导致其他 goroutine 饥饿。
以下是
  1. runtime.Gosched()
复制代码
函数的一些使用场景:

  • 避免饥饿:在长时间运行的循环中,如果确定当前 goroutine 可能不会被阻塞,可以调用
    1. Gosched
    复制代码
    来让出 CPU。
  • 控制执行顺序:在某些情况下,你可能希望控制 goroutine 的执行顺序,通过
    1. Gosched
    复制代码
    可以给其他 goroutine 运行的机会。
  • 减少 CPU 使用:在某些 I/O 密集型操作中,如果当前 goroutine 主要是等待 I/O 操作完成,调用
    1. Gosched
    复制代码
    可以让出 CPU,减少不必要的 CPU 使用。
  • 避免死锁:在某些复杂的 goroutine 调度中,如果担心死锁问题,可以在适当的地方调用
    1. Gosched
    复制代码
    来减少死锁的风险。
  1. package main

  2. import (
  3.         "fmt"
  4.         "runtime"
  5.         "time"
  6. )

  7. func main() {
  8.         /*
  9.                 因为goroutine2延时了一定时间,如果goroutine1不让出CPU时间片那么必先执行完成
  10.         */
  11.         go func() {
  12.                 for i := 0; i < 5; i++ {
  13.                         runtime.Gosched() // 让出 CPU 时间片
  14.                         fmt.Println("Goroutine 1:", i)
  15.                 }
  16.         }()

  17.         go func() {
  18.                 for i := 0; i < 5; i++ {
  19.                         time.Sleep(100 * time.Millisecond)
  20.                         fmt.Println("Goroutine 2:", i)
  21.                 }
  22.         }()

  23.         time.Sleep(3 * time.Second) // 等待两个 goroutine 执行完毕
  24. }
复制代码
请注意,过度使用
  1. Gosched
复制代码
可能会导致性能下降,因为频繁的调度会消耗额外的 CPU 资源。因此,应该在仔细考虑后,根据实际需要来使用
  1. Gosched
复制代码

查看协程数&CUP数
  1. package main

  2. import (
  3.         "fmt"
  4.         "runtime"
  5.         "sync"
  6. )

  7. func main() {
  8.         var wg sync.WaitGroup //创建并发组
  9.         fmt.Printf("当前运行的goroutine数量: %d\n", runtime.NumGoroutine())
  10.         //创建10个协程
  11.         for i := 0; i < 10; i++ {
  12.                 wg.Add(1)
  13.                 go func(id int) {
  14.                         defer wg.Done()
  15.                         fmt.Printf("第 %d 个协程在 running\n", id)
  16.                 }(i)
  17.         }
  18.         fmt.Printf("当前运行的goroutine数量: %d\n", runtime.NumGoroutine())
  19.         wg.Wait()

  20.         fmt.Printf("CPU核心数: %d\n", runtime.NumCPU())
  21. }
复制代码
四、互斥锁

在并发编程中会遇到的临界资源安全问题,可以采用互斥锁的方式来解决,后面也可通过通过通道channel来解决
临界资源:指并发环境中多个进程、线程、协程共享的资源

使用sync包下的锁解决临界资源安全问题(Mutex)
  1. package main

  2. import (
  3.         "fmt"
  4.         "sync"
  5.         "time"
  6. )

  7. // 定义全局变量 票库存为10张
  8. var tickets int = 10

  9. // 创建锁
  10. var mutexs sync.Mutex

  11. func main() {
  12.         //三个窗口同时售票
  13.         go saleTicket("售票口1")
  14.         go saleTicket("售票口2")
  15.         go saleTicket("售票口3")
  16.         time.Sleep(time.Second * 15) //等待售票完

  17. }

  18. // 售票函数
  19. func saleTicket(name string) {
  20.         for {
  21.                 // 在检查之前上锁
  22.                 mutexs.Lock()
  23.                 if tickets > 0 {
  24.                         time.Sleep(time.Second)
  25.                         fmt.Printf("%s剩余的票数为:%d\n", name, tickets)
  26.                         tickets--
  27.                 } else {
  28.                         fmt.Println("票已售完")
  29.                         break
  30.                 }
  31.                 //操作结束后解锁
  32.                 mutexs.Unlock()
  33.         }
  34. }
复制代码
sync包下的同步等待组(WaitGroup)
  1. package main

  2. import (
  3.         "fmt"
  4.         "sync"
  5.         "time"
  6. )

  7. var w sync.WaitGroup

  8. func main() {
  9.         // 公司最后关门的人   0
  10.         // wg.Add(2) 判断还有几个线程、计数  num=2
  11.         // wg.Done() 我告知我已经结束了  -1
  12.         w.Add(2)

  13.         go test11()
  14.         go test22()

  15.         fmt.Println("main等待ing")
  16.         w.Wait() // 等待 wg 归零,才会继续向下执行
  17.         fmt.Println("end")

  18.         // 理想状态:所有协程执行完毕之后,自动停止。
  19.         //time.Sleep(3 * time.Second)

  20. }
  21. func test11() {
  22.         for i := 0; i < 5; i++ {
  23.                 time.Sleep(1 * time.Second)
  24.                 fmt.Println("test1--", i)
  25.         }
  26.         w.Done()
  27. }
  28. func test22() {
  29.         defer w.Done()
  30.         for i := 0; i < 5; i++ {
  31.                 fmt.Println("test2--", i)
  32.         }
  33. }
复制代码
五、Channel通道


不要以共享内存的方式通信,而要以通信的方式共享内存

通道可以被认为是Goroutines通信的管道,类似于管道中的水从一端到另一端的流动,数据可以从一端发送到另一端,通过通道接收,GO语言中建议使用Channel通道来实现Goroutines之间的通信
GO从语言层面保证同一个时间只有一个goroutine能够访问channel里面的数据,使用channel来通信,通过通信来传递内存数据,使得内存数据在不同的goroutine中传递,而不是使用共享内存来通信
每个通道都有与其相关的类型,类型是通道允许传输的数据类型(通道的零值为nil,nil通道没有任何用处,因此通道必须使用类似于map和切片的方法定义)
1.png

一个通道发送和接收数据默认是阻塞的,当一个数据被发送到通道时,在发送语句中被阻塞,直到另一个Goroutine从通道中读取数据

关闭通道

发送者可以通过关闭通道来通知接收方不会有更多的数据被发送到通道
  1. close(ch)
复制代码
接收者可以在接收来自通道的数据时使用额外的变量来检查通道是否已关闭
  1. v,ok := <- ch
复制代码
当ok的值为true,表示成功的从通道中读取了一个数据value,通道关闭时仍然可以读(存)数据当ok的值为false,表示从一个封闭的通道读取数据,从闭通道读取的数据将是通道类型的零值

缓冲通道

缓冲通道是指一个通道,带有一个缓冲区,发送到一个缓冲通道只有在缓冲区满时才被阻塞,类似的,从缓冲通道接收的信息只有在为空时才会被阻塞,可以通过将额外的容量参数传递给make函数来创建缓冲通道,该函数指定缓冲区的大小
  1. package main

  2. import (
  3.         "fmt"
  4.         "strconv"
  5.         "time"
  6. )

  7. func main() {
  8.         //定义通道可以写10个数据
  9.         ch := make(chan string, 10)
  10.         go test3(ch)

  11.         for v := range ch {
  12.                 fmt.Println(v)
  13.         }
  14. }

  15. func test3(ch chan string) {
  16.         for i := 0; i < 5; i++ {
  17.                 time.Sleep(time.Second)
  18.                 fmt.Println("通道内写入数据", "tset--"+strconv.Itoa(i))
  19.                 ch <- "tset--" + strconv.Itoa(i)
  20.         }
  21.         close(ch)//如果不关闭协程,主协程的for循环一值阻塞,知道报错“fatal error: all goroutines are asleep - deadlock!”
  22. }
复制代码
定向通道

单向通道也就是定向通道,这些通道只能发送数据或者接收数据
  1. package main

  2. import (
  3.         "fmt"
  4.         "time"
  5. )

  6. func main() {
  7.         ch := make(chan int)
  8.         go writerOnly(ch)
  9.         go readOnly(ch)

  10.         time.Sleep(time.Second * 2)
  11. }

  12. // 只读,指只允许管道读入/写出数据
  13. func writerOnly(ch chan<- int) {
  14.         ch <- 10
  15. }

  16. // 只写,指指只允许管道读出/写入数据
  17. func readOnly(ch <-chan int) {
  18.         temp := <-ch
  19.         fmt.Println(temp)
  20. }
复制代码
Select


  • 每个case都必须是一个通道的操作
  • 如果任意某个通信可以进行,他就执行,其他被忽略
  • 如果有多个case都可以运行,Select会随机公平的选出一个执行
否则

  • 如果有default子句,则执行该语句
  • 如果没有default字句,select将阻塞,直到某个通信可以运行,GO不会重新对channel或值进行求值
  1. package main

  2. import (
  3.         "fmt"
  4.         "time"
  5. )

  6. func main() {

  7.         ch1 := make(chan int)
  8.         ch2 := make(chan int)

  9.         go func() {
  10.                 time.Sleep(time.Second * 2)
  11.                 ch1 <- 100
  12.         }()

  13.         go func() {
  14.                 time.Sleep(time.Second * 2)
  15.                 ch2 <- 200
  16.         }()

  17.         select {
  18.         case num1 := <-ch1:
  19.                 fmt.Println("ch1--", num1)
  20.         case num2 := <-ch2:
  21.                 fmt.Println("ch2--", num2)
  22.         }
  23.     //没有default,则等待通道(阻塞),因为select{}本身是阻塞的
  24. }
复制代码
利用通道解决临界资源安全问题
  1. package main

  2. import (
  3.         "fmt"
  4.         "sync"
  5.         "time"
  6. )

  7. // 定义全局chan,存储票总数
  8. var totalTickets chan int
  9. var wg sync.WaitGroup

  10. func main() {

  11.         // 初始化票数量:总票数10张
  12.         totalTickets = make(chan int, 2)
  13.         totalTickets <- 10

  14.         wg.Add(3)

  15.         go sell("售票口1")
  16.         go sell("售票口2")
  17.         go sell("售票口3")

  18.         wg.Wait()

  19.         fmt.Println("买完了,下班")

  20. }

  21. func sell(name string) {
  22.         defer wg.Done()

  23.         for { //for循环表示一直在卖,一直在营业
  24.                 residue, ok := <-totalTickets
  25.                 if !ok {
  26.                         fmt.Printf("%s: 关闭\n", name)
  27.                         break
  28.                 }
  29.                 if residue > 0 {
  30.                         time.Sleep(time.Second * 1)
  31.                         totalTickets <- residue - 1 //
  32.                         fmt.Println(name, "售出1张票,余票:", residue)
  33.                 } else {
  34.                         //进入此处时票已经买完了,因为for循环一进来售票窗口就检查是否还有票,假如最后卖完的是售票口3,那么销售窗口1跟2就会判断还有没有
  35.                         fmt.Printf("%s: 关闭\n", name)
  36.                         close(totalTickets)
  37.                         break
  38.                 }
  39.         }
  40. }
复制代码
到此这篇关于Go 并发编程Goroutine的实现示例的文章就介绍到这了,更多相关Go 并发Goroutine内容请搜索晓枫资讯以前的文章或继续浏览下面的相关文章希望大家以后多多支持晓枫资讯!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
晓枫资讯-科技资讯社区-免责声明
免责声明:以上内容为本网站转自其它媒体,相关信息仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同其观点或证实其内容的真实性。
      1、注册用户在本社区发表、转载的任何作品仅代表其个人观点,不代表本社区认同其观点。
      2、管理员及版主有权在不事先通知或不经作者准许的情况下删除其在本社区所发表的文章。
      3、本社区的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,举报反馈:点击这里给我发消息进行删除处理。
      4、本社区一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
      5、以上声明内容的最终解释权归《晓枫资讯-科技资讯社区》所有。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

  • 打卡等级:无名新人
  • 打卡总天数:1
  • 打卡月天数:0
  • 打卡总奖励:19
  • 最近打卡:2024-09-13 16:51:57
等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
32
积分
6
注册时间
2023-6-16
最后登录
2024-9-13

发表于 2025-10-22 06:42:37 | 显示全部楼层
路过,支持一下
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~
严禁发布广告,淫秽、色情、赌博、暴力、凶杀、恐怖、间谍及其他违反国家法律法规的内容。!晓枫资讯-社区
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

1楼
2楼

手机版|晓枫资讯--科技资讯社区 本站已运行

CopyRight © 2022-2025 晓枫资讯--科技资讯社区 ( BBS.yzwlo.com ) . All Rights Reserved .

晓枫资讯--科技资讯社区

本站内容由用户自主分享和转载自互联网,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。

如有侵权、违反国家法律政策行为,请联系我们,我们会第一时间及时清除和处理! 举报反馈邮箱:点击这里给我发消息

Powered by Discuz! X3.5

快速回复 返回顶部 返回列表