【Golang 面试 - 进阶题】每日 3 题(八)

✍个人博客:Pandaconda-CSDN博客

📣专栏地址:http://t.csdnimg.cn/UWz06

📚专栏简介:在这个专栏中,我将会分享 Golang 面试中常见的面试题给大家~

❤️如果有收获的话,欢迎点赞👍收藏📁,您的支持就是我创作的最大动力💪

22. Goroutine 定义

Goroutine 是 Go 语言中的一种轻量级线程实现,它可以在单个进程中同时执行多个任务,实现了并发编程。与传统的线程相比,Goroutine 的创建和切换开销非常小,因此可以轻松创建数以千计的 Goroutine,而不会导致系统资源的耗尽。

Goroutine 的定义非常简单,只需要在函数调用前添加关键字 go 即可创建一个 Goroutine。例如:

Go 复制代码
func main() {
    go sayHello()
}
func sayHello() {
    fmt.Println("Hello, world!")
}

在这个例子中,我们创建了一个 Goroutine 来执行 sayHello() 函数,使用关键字 go 来启动 Goroutine。当程序执行到 go sayHello() 时,会创建一个新的 Goroutine 来执行 sayHello() 函数,而主 Goroutine 则会继续执行下去,不会等待 sayHello() 函数执行完毕。

需要注意的是,Goroutine 是由 Go 运行时环境调度的,它们并不是线程或进程。每个 Goroutine 都是由 Go 运行时环境自动分配的,它们共享相同的地址空间和堆栈。因此,在 Goroutine 中共享内存需要采用同步机制来保证线程安全。

Goroutine 是 Go 语言的核心特性之一,它使得并发编程变得简单而高效。通过合理使用 Goroutine,可以充分发挥多核 CPU 的性能,提高程序的并发处理能力。

23. Go goroutine 的底层实现原理?

概念

Goroutine 可以理解为一种 Go 语言的协程(轻量级线程),是 Go 支持高并发的基础,属于用户态的线程,由 Go runtime 管理而不是操作系统。

底层数据结构

Go 复制代码
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 对象放入调度队列。

状态流转

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

创建

通过 go 关键字调用底层函数 runtime.newproc() 创建一个 goroutine

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

Go 复制代码
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 状态。

24. Go goroutine 泄露的场景?

泄露原因

  • Goroutine 内进行 channel/mutex 等读写操作被一直阻塞。

  • Goroutine 内的业务逻辑进入死循环,资源一直无法释放。

  • Goroutine 内的业务逻辑进入长时间等待,有不断新增的 Goroutine 进入等待

泄露场景

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

1. nil channel

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

Go 复制代码
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
        }()
    }
}

输出结果:

Go 复制代码
before goroutines:  1
after goroutines:  11

2. 发送不接收

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

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

3. 接收不发送

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

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

4. http request body 未关闭

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

Go 复制代码
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。

Go 复制代码
defer resp.Body.Close()

5. 互斥锁忘记解锁

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

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

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

6. sync.WaitGroup 使用不当

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

Go 复制代码
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 的数量。

相关推荐
F-2H几秒前
C语言:指针4(常量指针和指针常量及动态内存分配)
java·linux·c语言·开发语言·前端·c++
bryant_meng1 小时前
【python】OpenCV—Image Moments
开发语言·python·opencv·moments·图片矩
若亦_Royi2 小时前
C++ 的大括号的用法合集
开发语言·c++
慕城南风2 小时前
Go语言中的defer,panic,recover 与错误处理
golang·go
资源补给站2 小时前
大恒相机开发(2)—Python软触发调用采集图像
开发语言·python·数码相机
m0_748247553 小时前
Web 应用项目开发全流程解析与实战经验分享
开发语言·前端·php
刘大辉在路上3 小时前
突发!!!GitLab停止为中国大陆、港澳地区提供服务,60天内需迁移账号否则将被删除
git·后端·gitlab·版本管理·源代码管理
测试杂货铺3 小时前
Jmeter压测实战:Jmeter二次开发之自定义函数
自动化测试·软件测试·测试工具·jmeter·职场和发展·测试用例·压力测试
6.943 小时前
Scala学习记录 递归调用 练习
开发语言·学习·scala
FF在路上3 小时前
Knife4j调试实体类传参扁平化模式修改:default-flat-param-object: true
java·开发语言