Golang
goroutine内存泄漏
slice导致
获取长字符串中的一段,导致字符串未释放;
获取长slice中的一段导致长slice未释放;
在长切片中新建sllice导致泄漏
channel导致
发送不接受,接收不发送,nil channel
- 从 channel 里读,但是同时没有写入操作
- 向 无缓冲 channel 里写,但是同时没有读操作
- 向已满的 有缓冲 channel 里写,但是同时没有读操作
- select操作在所有case上都阻塞()
- goroutine进入死循环,一直结束不了
- 向 nil channel 发送和接收数据都将会导致阻塞。这种情况可能在我们定义 channel 时忘记初始化的时候发生。
可见,很多都是因为channel使用不当造成阻塞,从而导致goroutine也一直阻塞无法退出导致的。
传统同步方式sync.mutex,sync.waitgroup导致
用了mutex加lock之后忘记unlock;
在一开始设置了具体数目的wg.wait(n),但是有没有写够足够数量n的wg.Done(),导致wg.Wait()一直等待下去。(正确方式可以使用wg.Add(1)配合wg.Done使用)
Go调度器的GMP
在Go语言中,GPM通常指的是Goroutine、Processor和Machine,这是Go调度器(scheduler)的核心组成部分。下面是对每个部分的详细介绍:
Goroutine (G):
- Goroutine是Go语言中的轻量级线程,由Go运行时管理。它们是并发的基本单位,可以被创建和销毁,而无需操作系统级别的线程开销。Goroutine的创建和销毁非常快速,因此可以轻松地创建成千上万个Goroutine。
- Goroutine的调度是协作式的,这意味着一个Goroutine在执行时会自愿放弃CPU,让其他Goroutine有机会执行。这种协作式调度使得Go语言能够高效地利用多核处理器。
Processor (P):
- Processor是Go调度器中的一个抽象概念,代表一个逻辑处理器。每个P都有一个本地运行队列,用于存储待执行的Goroutine。P的数量可以通过环境变量或运行时设置来调整,通常设置为CPU的核心数。
- P的主要作用是管理Goroutine的执行。当一个Goroutine被调度到P上时,P会将其分配给一个可用的Machine(M)来执行。
Machine (M):
- Machine代表一个操作系统线程。M与P关联,负责执行Goroutine。一个M可以与多个P关联,但在任何给定时间,一个M只能执行一个P的Goroutine。
- M的主要作用是执行Goroutine的代码。当一个Goroutine被调度到M上时,M会执行该Goroutine的代码,直到该Goroutine自愿放弃CPU或被抢占。
Go调度器的工作原理是将Goroutine(G)分配到Processor(P)上,然后由Machine(M)执行。这种设计使得Go语言能够高效地利用多核处理器,并实现高并发。
在 Go 语言的运行时系统中,Goroutine(简称 G)有多种状态,用于描述它在不同时间点的执行情况。这些状态在 Go 的调度器(GMP 模型)中扮演重要角色。GMP 模型由 Goroutine(G)、工作线程(M)和处理器(P)三部分组成。以下是 G 的主要状态及其转变过程,以及它们与 GMP 模型的关系。
G 的状态
- _Gidle:空闲状态。Goroutine 尚未被使用或已经完成执行,等待被分配新任务。
- _Grunnable:可运行状态。Goroutine 已经准备好运行,等待被调度器选中运行。
- _Grunning:运行状态。Goroutine 正在运行中。
- _Gsyscall:系统调用状态。Goroutine 正在执行系统调用,处于阻塞状态,不会被调度器调度。
- _Gwaiting:等待状态。Goroutine 在等待某个条件(例如通道操作、定时器、网络 I/O 等)完成。
- _Gdead:死亡状态。Goroutine 已经完成执行,无法再被重新使用。
- _Gcopystack:堆栈复制状态。Goroutine 的堆栈正在被复制,以调整其大小。
状态转变及其与 GMP 的关系
创建 Goroutine
- _Gidle -> _Grunnable
- 创建一个新的 Goroutine,并将其状态设置为
_Grunnable
,表示该 Goroutine 准备好运行。 - 由 P 将新的 Goroutine 添加到其本地运行队列或全局运行队列中。
- 创建一个新的 Goroutine,并将其状态设置为
g := newGoroutine()
g.status = _Grunnable
p.runqput(g)
调度和运行 Goroutine
- _Grunnable -> _Grunning
- P 从本地运行队列或全局运行队列中取出一个 Goroutine,将其状态设置为
_Grunning
,并将其分配给一个 M 来执行。
- P 从本地运行队列或全局运行队列中取出一个 Goroutine,将其状态设置为
g := p.runqget()
g.status = _Grunning
m.execute(g)
系统调用
- _Grunning -> _Gsyscall
- Goroutine 在运行过程中进行系统调用,状态转变为
_Gsyscall
。 - 由于系统调用可能会阻塞,M 会寻找其他可运行的 Goroutine 来执行。
- Goroutine 在运行过程中进行系统调用,状态转变为
g.status = _Gsyscall
m.scheduleNextGoroutine()
- _Gsyscall -> _Grunnable
- 系统调用完成后,Goroutine 状态从
_Gsyscall
转变为_Grunnable
,等待再次被调度运行。
- 系统调用完成后,Goroutine 状态从
g.status = _Grunnable
p.runqput(g)
等待和唤醒
- _Grunning -> _Gwaiting
- Goroutine 在运行过程中等待某个条件完成,例如通道操作,状态转变为
_Gwaiting
。
- Goroutine 在运行过程中等待某个条件完成,例如通道操作,状态转变为
g.status = _Gwaiting
m.scheduleNextGoroutine()
- _Gwaiting -> _Grunnable
- 等待的条件满足后,Goroutine 状态从
_Gwaiting
转变为_Grunnable
,等待被再次调度运行。
- 等待的条件满足后,Goroutine 状态从
g.status = _Grunnable
p.runqput(g)
结束执行
- _Grunning -> _Gdead
- Goroutine 执行完毕,状态转变为
_Gdead
,等待被回收。
- Goroutine 执行完毕,状态转变为
g.status = _Gdead
GMP 模型与状态转变的关系
- G(Goroutine):G 是 Go 语言中的轻量级线程。G 的状态在其生命周期中不断变化,GMP 模型通过调度 G 来实现高效并发。
- M(Machine):M 是实际的操作系统线程,负责执行 G。M 可以在不同的 P 上运行不同的 G。
- P(Processor):P 是调度器的抽象,管理本地运行队列中的 G,并调度 M 来执行 G。每个 P 持有一个本地的 Goroutine 队列。
总结
G 的状态转变和 GMP 模型的关系密不可分。Goroutine 从创建到运行、等待、系统调用和结束,状态不断变化,而这些状态变化由 P 管理的运行队列和 M 执行 G 来实现。GMP 模型确保了 Go 语言的高效并发能力,通过合理的状态管理和调度,充分利用系统资源,优化 Goroutine 的执行。
一个goroutine阻塞,会不会影响其他的goroutine?
一个 Goroutine 阻塞不会影响其他的 Goroutine。Go 的运行时调度器会自动将阻塞的 Goroutine 挂起,并调度其他可运行的 Goroutine 执行。因此,多个 Goroutine 可以并发执行,即使其中一些 Goroutine 阻塞了,也不会影响其他 Goroutine 的执行。
Channel是怎么实现的?
Go语言中的通道(channel)是实现并发编程的核心机制之一。通道提供了一种在多个goroutine之间安全地传递数据的方式。通道的实现是Go运行时系统的一部分,它基于一种称为“goroutine-safe”的队列数据结构,并结合了锁和条件变量来确保并发安全性。
以下是Go通道实现的一些关键点:
数据结构:
hchan
结构体:这是Go运行时系统中用于表示通道的核心数据结构。它包含了通道的各种属性,如缓冲区、发送队列、接收队列、锁、类型信息等。sudog
结构体:用于表示等待在通道上的goroutine。每个等待的goroutine都会被包装成一个sudog
对象,并放入发送队列或接收队列中。
缓冲区:
- 通道可以是缓冲的或非缓冲的。缓冲通道有一个固定大小的缓冲区,用于存储发送的数据,直到它们被接收。非缓冲通道没有缓冲区,发送和接收操作是同步的。
锁:
- 通道使用互斥锁(
mutex
)来保护其内部数据结构,确保在任何时候只有一个goroutine可以访问通道的状态。
- 通道使用互斥锁(
条件变量:
- 通道使用条件变量来实现goroutine的阻塞和唤醒。当goroutine尝试向满的缓冲通道发送数据或从空的缓冲通道接收数据时,它们会被阻塞,并放入相应的发送队列或接收队列中。
发送和接收操作:
- 发送操作(
ch <- value
):如果通道有空间(对于非缓冲通道,意味着有等待的接收者;对于缓冲通道,意味着缓冲区未满),则数据被复制到通道的缓冲区或直接传递给接收者。否则,发送goroutine会被阻塞,直到通道有空间。 - 接收操作(
value := <-ch
):如果通道有数据(对于非缓冲通道,意味着有等待的发送者;对于缓冲通道,意味着缓冲区非空),则数据被复制到接收者的变量中。否则,接收goroutine会被阻塞,直到通道有数据。
- 发送操作(
关闭操作:
- 关闭操作(
close(ch)
):关闭通道会释放所有等待的goroutine,并通知它们通道已关闭。关闭一个已经关闭的通道或一个nil
通道都会导致panic。
- 关闭操作(
垃圾回收:
- 通道的内存管理与Go的垃圾回收机制集成。当通道不再被引用时,它会被垃圾回收器回收。
Go通道的实现确保了goroutine之间的同步和通信是高效且安全的。通过使用通道,开发者可以避免显式地处理锁和其他并发控制机制,从而简化了并发编程。
Context
在Go语言中,context
(上下文)是一个标准库包,它提供了一种在goroutine之间传递请求范围的值、取消信号和截止时间的机制。context
包的核心是Context
接口,它定义了四个方法:
Deadline
:返回Context
被取消的时间,也就是完成工作的截止时间。Done
:返回一个<-chan struct{}
,当Context
被取消或者超时时,该通道会被关闭,从而发出信号。Err
:返回Context
被取消的原因。Value
:返回与Context
关联的键值对数据。
context
包的主要用途包括:
取消操作:当一个请求被取消或超时时,可以使用
context
来通知所有处理该请求的goroutine停止工作,从而避免不必要的资源消耗。传递请求范围的值:在处理请求的过程中,可能需要在多个goroutine之间共享一些数据,如认证令牌、请求ID等。
context
提供了一种方便的方式来传递这些数据。截止时间管理:在处理请求时,可能需要设置一个截止时间,超过这个时间后,无论工作是否完成,都应该停止工作。
context
可以用来设置和管理这些截止时间。
使用context
的基本模式是:
- 在处理请求的顶级goroutine中创建一个
Context
对象,通常是通过调用context.Background()
或context.TODO()
来获取一个空的Context
,然后使用context.WithCancel
、context.WithDeadline
、context.WithTimeout
或context.WithValue
来创建一个具体的Context
。 - 将这个
Context
传递给处理请求的所有goroutine。 - 在goroutine中,通过检查
Context
的Done
通道来判断是否应该停止工作。 - 使用
Context
的Value
方法来获取请求范围的值。
例如,以下是一个使用context
来取消操作的简单示例:
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 创建一个可以取消的Context
ctx, cancel := context.WithCancel(context.Background())
// 启动一个goroutine来执行任务
go func() {
printLine("Working...")
time.Sleep(2 * time.Second) // 模拟工作
printLine("Work done.")
}()
// 等待一段时间后取消任务
time.Sleep(1 * time.Second)
cancel()
// 等待goroutine结束
time.Sleep(2 * time.Second)
printLine("Main function done.")
}
func printLine(s string) {
fmt.Println(s, time.Now().Format(time.RFC3339))
}
在这个例子中,context.WithCancel
创建了一个可以取消的Context
,cancel
函数用于发出取消信号。当cancel
被调用时,所有监听ctx.Done()
通道的goroutine都会收到信号,从而知道应该停止工作。
Go的内存分布
Go 的内存分布主要包括以下几个区域:
栈内存(Stack Memory):
- 每个 Goroutine 都有一个独立的栈内存,初始大小很小(如 2KB),但可以动态增长。
- 栈内存用于存储局部变量、函数参数和返回值等。
堆内存(Heap Memory):
- 动态分配的内存块(通过
new
和make
分配)存储在堆上。 - 堆内存由 Go 的垃圾回收器管理。
- 动态分配的内存块(通过
全局/静态内存(Global/Static Memory):
- 用于存储全局变量和静态变量。
- 在程序的整个生命周期内都存在。
文本段(Text Segment):
- 存储程序的代码,即可执行指令。
数据段(Data Segment):
- 存储已初始化的全局变量和静态变量。
Go的GC(垃圾回收)
Go 的垃圾回收器是一个非分代、并发标记清除垃圾回收器。其工作过程如下:
标记阶段(Marking Phase):
- 垃圾回收器遍历所有的可达对象并标记它们。
- 可达对象是指从根对象(全局变量、栈变量、寄存器等)开始,沿着引用链可以访问到的对象。
清除阶段(Sweeping Phase):
- 标记阶段结束后,垃圾回收器会清除未标记的对象,并将这些对象的内存归还给堆。
Go 的垃圾回收器是并发的,这意味着它可以与应用程序的 Goroutine 同时运行,以减少垃圾回收带来的停顿时间(STW,Stop-The-World)。
sync.Map是如何实现并发安全的?
sync.Map
是 Go 提供的一个并发安全的 map 实现,主要通过以下方式实现并发安全:
读写分离:
- 使用两个不同的数据结构(read 和 dirty)来存储数据,read 用于大多数读取操作,dirty 用于写入操作。
- 读取操作不会锁住整个 map,而是直接访问 read 数据结构。
原子操作:
- 对于读取操作,使用原子操作来确保并发安全。
- 当写入操作需要修改 dirty map 时,会使用互斥锁(
sync.Mutex
)来确保安全。
惰性初始化:
- 如果 dirty map 的某个键被访问多次,就会将它提升到 read map,以减少锁竞争。
鸭子类型
不要求显式的定义对象的类型,只要这个对象实现了接口中的方法,就可以视作同一类型。
package main
import "fmt"
// 定义一个接口
type Quacker interface {
Quack()
}
// 定义一个Duck结构体
type Duck struct{}
func (d Duck) Quack() {
fmt.Println("Quack!")
}
// 定义一个Person结构体
type Person struct{}
func (p Person) Quack() {
fmt.Println("I'm not a duck, but I can quack!")
}
func makeItQuack(q Quacker) {
q.Quack()
}
func main() {
var d Duck
var p Person
makeItQuack(d) // 输出:Quack!
makeItQuack(p) // 输出:I'm not a duck, but I can quack!
}
在上述代码中,Duck
和 Person
都实现了 Quacker
接口中的 Quack
方法,因此它们都可以作为 Quacker
类型的参数传递给 makeItQuack
函数。