Go语言设计与实现 --Goroutine

news2024/12/29 10:12:36

Goroutine是GMP模型中的G,是属于用户态的线程,由Go runtime管理,而不是操作系统管理。

数据结构

type g struct {
    goid    int64 // 唯一的goroutine的ID
    sched gobuf // goroutine切换时,用于保存g的上下文
    stack stack // 栈
  	gopc        // pc of go statement that created this goroutine
    startpc    uintptr // pc of goroutine function
    ...
}

type gobuf struct {
    sp   uintptr // 栈指针位置
    pc   uintptr // 运行到的程序位置
    g    guintptr // 指向 goroutine
    ret  uintptr  // 保存系统调用的返回值
    ...
}

type stack struct {
    lo uintptr // 栈的下界内存地址
    hi uintptr // 栈的上界内存地址
}

最终有一个 runtime.g 对象放入调度队列

Goroutine状态

状态含义
空闲中_GidleG刚刚新建,没有初始化
待运行_Grunnable就绪状态,G在运行队列中,等待M取出并运行
运行中_GrunningM正在运行这个G,这时候M会拥有一个P
系统调用中_GsyscallM正在运行这个G发起的系统调用,这时候M并不拥有P
等待中_GwaitingG在等待某些条件完成,这时候G不在运行也不在运行队列中(可能在channel的等待队列中)
已中止_GdeadG未被使用,可能已经执行完毕
栈复制中_GcopystackG正在获取一个新的栈空间并把原来的内容复制过去(用于防止GC扫描)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wlBjgZpx-1672994559533)(https://image-1302243118.cos.ap-beijing.myqcloud.com/imgcdn/5.2.goroutine_state.jpg)]

创建

通过go关键字调用runtime.newproc()创建一个Goroutine。当调用该函数之后,Goroutine会被设置成runnable状态。

func main() {
   go func() {
      fmt.Println("func routine")
   }()
   fmt.Println("main goroutine")
}

创建好的这个Goroutine会新建一个自己的栈空间,同时在G的sched中维护栈地址和程序计数器这些信息。

每个 G 在被创建之后,都会被优先放入到本地队列中,如果本地队列已经满了,就会被放入到全局队列中。

运行

goroutine 本身只是一个数据结构,真正让 goroutine 运行起来的是调度器。Go 实现了一个用户态的调度器(GMP模型),这个调度器充分利用现代计算机的多核特性,同时让多个 goroutine 运行,同时 goroutine 设计的很轻量级,调度和上下文切换的代价都比较小。

调度时机

  • 新起一个协程和协程执行完毕
  • 会阻塞的系统调用,比如文件io、网络io
  • channel、mutex等阻塞操作
  • time.sleep
  • 垃圾回收之后
  • 主动调用runtime.Gosched()
  • 运行过久或系统调用过久等等

每个 M 开始执行 P 的本地队列中的 G时,goroutine会被设置成running状态

如果某个 M 把本地队列中的G都执行完成之后,然后就会去全局队列中拿 G,这里需要注意,每次去全局队列拿 G 的时候,都需要上锁,避免同样的任务被多次拿。

如果全局队列都被拿完了,而当前 M 也没有更多的 G 可以执行的时候,它就会去其他 P 的本地队列中拿任务,这个机制被称之为 work stealing 机制,每次会拿走一半的任务,向下取整,比如另一个 P 中有 3 个任务,那一半就是一个任务。

当全局队列为空,M 也没办法从其他的 P 中拿任务的时候,就会让自身进入自旋状态,等待有新的 G 进来。最多只会有 GOMAXPROCS 个 M 在自旋状态,过多 M 的自旋会浪费 CPU 资源。

阻塞

channel的读写操作、等待锁、等待网络数据、系统调用等都有可能发生阻塞,会调用底层函数runtime.gopark(),会让出CPU时间片,让调度器安排其它等待的任务运行,并在下次某个时候从该位置恢复执行。

当调用该函数之后,goroutine会被设置成waiting状态

唤醒

处于waiting状态的goroutine,在调用runtime.goready()函数之后会被唤醒,唤醒的goroutine会被重新放到M对应的上下文P对应的runqueue中,等待被调度。

当调用该函数之后,goroutine会被设置成runnable状态

退出

当goroutine执行完成后,会调用底层函数runtime.Goexit()

当调用该函数之后,goroutine会被设置成dead状态

Goroutine和线程的区别

Goroutine线程
内存占用创建一个Goroutine的栈内存消耗为2kb,实际运行过程中,如果栈空间不够用会自动进行扩容。消耗的栈空间大概要1MB
创建和销毁用户级的,由Go runtime负责,创建和销毁代价非常小创建和销毁代价很大,内核级别,常用解决方法是线程池
切换Goroutine切换只需要3哥寄存器PC,SP,BP。goroutine 的切换约为 200 ns,相当于 2400-3600 条指令。当线程切换时,需要保存各种寄存器,以便恢复现场。线程切换会消耗 1000-1500 ns,相当于 12000-18000 条指令。

Goroutine泄漏

泄漏原因

  • Goroutine 内进行channel/mutex 等读写操作被一直阻塞。
  • Goroutine 内的业务逻辑进入死循环,资源一直无法释放。
  • Goroutine 内的业务逻辑进入长时间等待,有不断新增的 Goroutine 进入等待

泄漏场景

如果输出的 goroutines 数量是在不断增加的,就说明存在泄漏

nil channel

channel 如果忘记初始化,那么无论你是读,还是写操作,都会造成阻塞。

func main() {
    fmt.Println("before goroutines: ", runtime.NumGoroutine())
    block1()
    time.Sleep(time.Second * 1)
    fmt.Println("after goroutines: ", runtime.NumGoroutine())
}

func block1() {
    var ch chan int
    for i := 0; i < 10; i++ {
        go func() {
            <-ch
        }()
    }
}

输出结果:

before goroutines:  1
after goroutines:  11

发送不接收

channel 发送数量 超过 channel接收数量,就会造成阻塞

func block2() {
    ch := make(chan int)
    for i := 0; i < 10; i++ {
        go func() {
            ch <- 1
        }()
    }
}

接收不发送

channel 接收数量 超过 channel发送数量,也会造成阻塞

func block3() {
    ch := make(chan int)
    for i := 0; i < 10; i++ {
        go func() {
            <-ch
        }()
    }
}

http request body未关闭

resp.Body.Close() 未被调用时,goroutine不会退出

func requestWithNoClose() {
    _, err := http.Get("https://www.baidu.com")
    if err != nil {
        fmt.Println("error occurred while fetching page, error: %s", err.Error())
    }
}

func requestWithClose() {
    resp, err := http.Get("https://www.baidu.com")
    if err != nil {
        fmt.Println("error occurred while fetching page, error: %s", err.Error())
        return
    }
    defer resp.Body.Close()
}

func block4() {
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
                defer wg.Done()
                requestWithNoClose()
        }()
    }
}

var wg = sync.WaitGroup{}

func main() {
    block4()
    wg.Wait()
}

一般发起http请求时,需要确保关闭body

defer resp.Body.Close()

互斥锁忘记解锁

第一个协程获取 sync.Mutex 加锁了,但是他可能在处理业务逻辑,又或是忘记 Unlock 了。

因此导致后面的协程想加锁,却因锁未释放被阻塞了

func block5() {
    var mutex sync.Mutex
    for i := 0; i < 10; i++ {
        go func() {
            mutex.Lock()
        }()
    }
}

sync.WaitGroup使用不当

由于 wg.Add 的数量与 wg.Done 数量并不匹配,因此在调用 wg.Wait 方法后一直阻塞等待

func block6() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        go func() {
            wg.Add(2)
            wg.Done()
            wg.Wait()
        }()
    }
}

如何排查

单个函数:调用 runtime.NumGoroutine 方法来打印 执行代码前后Goroutine 的运行数量,进行前后比较,就能知道有没有泄露了。

生产/测试环境:使用PProf实时监测Goroutine的数量

import (
   "net/http"
    // 注意,我们需要导入这个包
   _ "net/http/pprof"
)

func main() {
   for i := 0; i < 100; i++ {
      go func() {
         select {}
      }()
   }

   go func() {
      http.ListenAndServe("localhost:8080", nil)
   }()

   select {}
}

然后启动程序,启动程序之后输入:

go tool pprof -http=:1248 http://127.0.0.1:8080/debug/pprof/goroutine

注意这个端口号要和你监听的一致,并且要安装graphviz,把bin目录加入到Path环境变量中。

会自动打开你默认游览器。

在这里插入图片描述

可以看到有103个Goroutine在运行。

如何控制并发的Goroutine数量?

在开发过程中,如果不对goroutine加以控制而进行滥用的话,可能会导致服务整体崩溃。比如耗尽系统资源导致程序崩溃,或者CPU使用率过高导致系统忙不过来

有缓存channel

利用缓冲满时发送阻塞的特性。

var wg = sync.WaitGroup{}

func main() {
   // 模拟用户请求数量
   requestCount := 10
   fmt.Println("goroutine_num", runtime.NumGoroutine())
   // 管道长度就是最大并发数
   ch := make(chan bool, 3)
   for i := 0; i < requestCount; i++ {
      wg.Add(1)
      ch <- true
      go Read(ch, i)
   }
   wg.Wait()
}

func Read(ch chan bool, i int) {
   fmt.Printf("goroutine_num: %d, go func: %d\n", runtime.NumGoroutine(), i)
   <-ch
   wg.Done()
}

运行结果如下:

goroutine_num 1
goroutine_num: 4, go func: 2
goroutine_num: 4, go func: 3
goroutine_num: 4, go func: 4
goroutine_num: 4, go func: 5
goroutine_num: 4, go func: 6
goroutine_num: 4, go func: 7
goroutine_num: 4, go func: 8
goroutine_num: 4, go func: 0
goroutine_num: 4, go func: 1
goroutine_num: 4, go func: 9

无缓冲channel

任务发送和执行分离,指定消费者并发协程数

var wg = sync.WaitGroup{}

func main() {
    // 模拟用户请求数量
    requestCount := 10
    fmt.Println("goroutine_num", runtime.NumGoroutine())
    ch := make(chan bool)
    for i := 0; i < 3; i++ {
        go Read(ch, i)
    }

    for i := 0; i < requestCount; i++ {
        wg.Add(1)
        ch <- true
    }

    wg.Wait()
}

func Read(ch chan bool, i int) {
    for _ = range ch {
        fmt.Printf("goroutine_num: %d, go func: %d\n", runtime.NumGoroutine(), i)
        wg.Done()
    }
}

说实话这样做效率太过底下了,因为用了阻塞等待,生产环境基本上不可能这么去操作的

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/144221.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Android Studio调用so库中方法

一、JNI规范so库调用 在 Android Studio生成自己的so库 中已经创建了自己的so库&#xff0c;这是一个JNI规范的so库&#xff0c;可以直接将so库放到libs中&#xff0c;并按照上面文章中MainActivity中的调用方法使用。 1、build.gradle&#xff08;app&#xff09;配置 andro…

SHELL脚本学习 --- 第六次作业(正则和sed)

SHELL脚本学习 — 第六次作业 思路&#xff1a; 作业1&#xff1a; 1&#xff0c;正则匹配h或H即可 2&#xff0c;sh$匹配以sh结尾 3&#xff0c;使用[[:space:]]匹配空格&#xff0c;[^[:space:]]匹配非空格 4&#xff0c;^to开头&#xff0c;中间.匹配任意字符0次或多次&…

Java一学就会系列:介绍与第一个java程序

系列文章目录 java环境-jdk环境安装与配置&#xff08;jdk1.8&#xff09; 文章目录系列文章目录前言一、JAVA是什么&#xff1f;二、环境安装三、开发工具1. Eclipse&#xff08;推荐&#xff09;2. IntelliJ IDEA &#xff08;收费&#xff09;四、第一个Java程序总结前言 …

搭建Django项目——实现简单的API访问

1、创建Django项目 打开pycharm&#xff0c;新建Django项目&#xff0c;可以选择一个虚拟环境 建完之后目录如下&#xff1a; 2、创建应用&#xff0c;我这里命名为demo 在命令行执行 python manage.py startapp demo执行之后&#xff0c;会发现项目目录下多了demo文件夹…

Linux小黑板(6):软硬链接

"飞吧&#xff0c;去寻觅红色的流星!"一、软硬链接简介软链接:软链接又叫符号链接&#xff0c;这个文件包含了另一个文件的路径名。可以是任意文件或目录&#xff0c;可以链接不同文件系统的文件。软链接硬链接:硬链接&#xff08;hard link&#xff0c;也称链接&…

美创科技深度参编,中国信通院《数据安全治理实践指南(2.0)》发布

1月5日&#xff0c;由中国信息通信研究院、中国通信标准化协会指导&#xff0c;中国通信标准化协会大数据技术标准推进委员会主办&#xff0c;数据安全推进计划承办的第二届数据安全治理峰会成功召开&#xff0c;多项数据安全研究重要成果发布。会上&#xff0c;美创科技参与编…

MyBatis中数组套数组的格式

数组套数组的形式写法 1.dao层 List<Regulation> queryAllRegulations(); 配置 <resultMap id"RegulationResultMap" type"com.elfsack.cs.dto.allot.Regulation"><result column"shop_code" property"shopCode" /…

ARM32平台系统crash(系统崩溃) 问题定位的一种解决方法

说明 分享一种crash问题定位的一种解决方法&#xff0c;仅供参考。 ARM32平台上通过错误使用内存&#xff0c;触发系统异常&#xff0c;系统崩溃。系统异常被挂起后&#xff0c;能在串口中看到异常调用栈打印信息和关键寄存器信息。 如下所示&#xff0c;excType表示异常类…

上传文件前后端处理【vue3 + springboot】

前端 1.处理modal框 <template><n-modalv-model:show"modalVisible"preset"card":title"title"class"w-700px"><n-space class"w-full pt-16px" :size"24" justify"end"><n-but…

python数据分析:采集分析岗位数据,看看薪资的高低都受什么因素影响呢

前言 嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 又到了学Python时刻~ 在我们学习的时候,通常会产生疑问:这个行业前景好不好呢? 今天我们就用python的数据分析这个就业方向来举例 看一下都有哪些因素影响了薪资的高低呢&#xff1f; 数据采集 模块使用: reques…

这些js原型及原型链面试题你能做对几道

一、前言 在面试过程中&#xff0c;频频被原型相关知识问住&#xff0c;每次回答都支支吾吾。后来有家非常心仪的公司&#xff0c;在二面时&#xff0c;果不其然&#xff0c;又问原型了&#xff01; 我痛下决心用了两天时间钻研了下原型&#xff0c;弄明白后发现世界都明亮了…

Spark 在 KaiwuDB 中的应用与实践

线上沙龙-技术流第 24 期营业啦01月12日&#xff08;周四&#xff09;19:30开务数据库 - B站直播间当数据库面对大量数据复杂 OLAP 查询时&#xff0c;性能出现局限性&#xff0c;无法满足用户 AP 方面的高性能要求。为此&#xff0c;KaiwuDB 推出了此项解决方案&#xff1a;借…

【NI Multisim 14.0原理图环境设置——电路图属性设置】

目录 序言 一、电路图属性设置 &#x1f34a;1.设置对象可见性 &#x1f34a;2.设置图纸颜色 &#x1f34a;3.设置图纸尺寸 &#x1f34a;4.设置图纸方向 &#x1f34a;5.设置图纸单位 &#x1f34a;6.设置图纸网格点 &#x1f34a;7.设置图纸边框 &#x1f34a;8. 设…

ELK安装使用

太久没用了&#xff0c;熟悉一下。 JDK1.8以上环境 下载地址 elasticsearch&#xff1a;https://www.elastic.co/cn/downloads/elasticsearch kibana: https://www.elastic.co/cn/downloads/kibana logstash &#xff1a; https://www.elastic.co/cn/downloads/logstash…

社招前端二面必会手写面试题总结

字符串查找 请使用最基本的遍历来实现判断字符串 a 是否被包含在字符串 b 中&#xff0c;并返回第一次出现的位置&#xff08;找不到返回 -1&#xff09;。 a34;b1234567; // 返回 2 a35;b1234567; // 返回 -1 a355;b12354355; // 返回 5 isContain(a,b);function isContain(a,…

RS—|下载Landsat8/9数据并进行ENVI大气校正(FLAASH模型)

文章目录1、 数据的下载。2、 辐射定标3、大气校正1、 数据的下载。 下载网址&#xff1a;链接: GloVis (usgs.gov)。下载的数据为2022年8月1日湖南省北部的遥感影像数据。该数据为L1级产品&#xff0c;只经过了几何校正&#xff0c;没有经过辐射定标和大气校正。 图1-1.下载…

富而喜悦2023直播盛典 唐苓馨主题演说“特别的礼物”!

网讯2023年1月1日19:30&#xff0c;富而喜悦一年一渡“特别的礼物”新年主题直播盛典晚会如约而至。富而喜悦品牌创始人唐苓馨女士&#xff0c;用自己与身边人的真实故事&#xff0c;为您讲述了“遇见生活中特别的礼物”。以下是富而喜悦品牌创始人唐苓馨女士在2023富而喜悦一年…

手写RPC框架05-通过SPI机制增加框架的扩展性的设计与实现

源码地址&#xff1a;https://github.com/lhj502819/IRpc/tree/v6 系列文章&#xff1a; 注册中心模块实现路由模块实现序列化模块实现过滤器模块实现自定义SPI机制增加框架的扩展性的设计与实现 现有的问题 在上一章节末尾我们提到了&#xff0c;目前我们的RPC框架可扩展性…

发表计算机SCI论文需要注意什么? - 易智编译EaseEditing

一篇SCI&#xff0c;除了能让审稿人浅显易懂的了解你的表达之外&#xff0c;我们还需要在内容上做好&#xff1a; 1、SCI论文标题创新、简洁 创新是因为写科技文章的目的在于报道新的科技进展&#xff0c;缺乏创新因素就会失去发表的意义。 但运用创新要建立在已有的科研成果…

QT部件透明阴影效果与不规则窗体

透明效果原始效果设置整个窗体透明&#xff0c;调用setWindowOpacity( )方法&#xff0c;传入一个0~1之间的值来表示透明度&#xff1b;1表示不透明&#xff0c;0表示完全透明setWindowOpacity(0.5);//0~1之间设置窗体透明&#xff0c;部件不透明setWindowFlags(Qt::FramelessW…