Golang

goroutine内存泄漏

slice导致

获取长字符串中的一段,导致字符串未释放;

获取长slice中的一段导致长slice未释放;

在长切片中新建sllice导致泄漏

channel导致

发送不接受,接收不发送,nil channel

  1. 从 channel 里读,但是同时没有写入操作
  2. 向 无缓冲 channel 里写,但是同时没有读操作
  3. 向已满的 有缓冲 channel 里写,但是同时没有读操作
  4. select操作在所有case上都阻塞()
  5. goroutine进入死循环,一直结束不了
  6. 向 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)的核心组成部分。下面是对每个部分的详细介绍:

  1. Goroutine (G):

    • Goroutine是Go语言中的轻量级线程,由Go运行时管理。它们是并发的基本单位,可以被创建和销毁,而无需操作系统级别的线程开销。Goroutine的创建和销毁非常快速,因此可以轻松地创建成千上万个Goroutine。
    • Goroutine的调度是协作式的,这意味着一个Goroutine在执行时会自愿放弃CPU,让其他Goroutine有机会执行。这种协作式调度使得Go语言能够高效地利用多核处理器。
  2. Processor (P):

    • Processor是Go调度器中的一个抽象概念,代表一个逻辑处理器。每个P都有一个本地运行队列,用于存储待执行的Goroutine。P的数量可以通过环境变量或运行时设置来调整,通常设置为CPU的核心数。
    • P的主要作用是管理Goroutine的执行。当一个Goroutine被调度到P上时,P会将其分配给一个可用的Machine(M)来执行。
  3. 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 的状态

  1. _Gidle:空闲状态。Goroutine 尚未被使用或已经完成执行,等待被分配新任务。
  2. _Grunnable:可运行状态。Goroutine 已经准备好运行,等待被调度器选中运行。
  3. _Grunning:运行状态。Goroutine 正在运行中。
  4. _Gsyscall:系统调用状态。Goroutine 正在执行系统调用,处于阻塞状态,不会被调度器调度。
  5. _Gwaiting:等待状态。Goroutine 在等待某个条件(例如通道操作、定时器、网络 I/O 等)完成。
  6. _Gdead:死亡状态。Goroutine 已经完成执行,无法再被重新使用。
  7. _Gcopystack:堆栈复制状态。Goroutine 的堆栈正在被复制,以调整其大小。

状态转变及其与 GMP 的关系

创建 Goroutine

  1. _Gidle -> _Grunnable
    • 创建一个新的 Goroutine,并将其状态设置为 _Grunnable,表示该 Goroutine 准备好运行。
    • 由 P 将新的 Goroutine 添加到其本地运行队列或全局运行队列中。
g := newGoroutine()
g.status = _Grunnable
p.runqput(g)

调度和运行 Goroutine

  1. _Grunnable -> _Grunning
    • P 从本地运行队列或全局运行队列中取出一个 Goroutine,将其状态设置为 _Grunning,并将其分配给一个 M 来执行。
g := p.runqget()
g.status = _Grunning
m.execute(g)

系统调用

  1. _Grunning -> _Gsyscall
    • Goroutine 在运行过程中进行系统调用,状态转变为 _Gsyscall
    • 由于系统调用可能会阻塞,M 会寻找其他可运行的 Goroutine 来执行。
g.status = _Gsyscall
m.scheduleNextGoroutine()
  1. _Gsyscall -> _Grunnable
    • 系统调用完成后,Goroutine 状态从 _Gsyscall 转变为 _Grunnable,等待再次被调度运行。
g.status = _Grunnable
p.runqput(g)

等待和唤醒

  1. _Grunning -> _Gwaiting
    • Goroutine 在运行过程中等待某个条件完成,例如通道操作,状态转变为 _Gwaiting
g.status = _Gwaiting
m.scheduleNextGoroutine()
  1. _Gwaiting -> _Grunnable
    • 等待的条件满足后,Goroutine 状态从 _Gwaiting 转变为 _Grunnable,等待被再次调度运行。
g.status = _Grunnable
p.runqput(g)

结束执行

  1. _Grunning -> _Gdead
    • Goroutine 执行完毕,状态转变为 _Gdead,等待被回收。
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通道实现的一些关键点:

  1. 数据结构

    • hchan 结构体:这是Go运行时系统中用于表示通道的核心数据结构。它包含了通道的各种属性,如缓冲区、发送队列、接收队列、锁、类型信息等。
    • sudog 结构体:用于表示等待在通道上的goroutine。每个等待的goroutine都会被包装成一个sudog对象,并放入发送队列或接收队列中。
  2. 缓冲区

    • 通道可以是缓冲的或非缓冲的。缓冲通道有一个固定大小的缓冲区,用于存储发送的数据,直到它们被接收。非缓冲通道没有缓冲区,发送和接收操作是同步的。
    • 通道使用互斥锁(mutex)来保护其内部数据结构,确保在任何时候只有一个goroutine可以访问通道的状态。
  3. 条件变量

    • 通道使用条件变量来实现goroutine的阻塞和唤醒。当goroutine尝试向满的缓冲通道发送数据或从空的缓冲通道接收数据时,它们会被阻塞,并放入相应的发送队列或接收队列中。
  4. 发送和接收操作

    • 发送操作(ch <- value):如果通道有空间(对于非缓冲通道,意味着有等待的接收者;对于缓冲通道,意味着缓冲区未满),则数据被复制到通道的缓冲区或直接传递给接收者。否则,发送goroutine会被阻塞,直到通道有空间。
    • 接收操作(value := <-ch):如果通道有数据(对于非缓冲通道,意味着有等待的发送者;对于缓冲通道,意味着缓冲区非空),则数据被复制到接收者的变量中。否则,接收goroutine会被阻塞,直到通道有数据。
  5. 关闭操作

    • 关闭操作(close(ch)):关闭通道会释放所有等待的goroutine,并通知它们通道已关闭。关闭一个已经关闭的通道或一个nil通道都会导致panic。
  6. 垃圾回收

    • 通道的内存管理与Go的垃圾回收机制集成。当通道不再被引用时,它会被垃圾回收器回收。

Go通道的实现确保了goroutine之间的同步和通信是高效且安全的。通过使用通道,开发者可以避免显式地处理锁和其他并发控制机制,从而简化了并发编程。

Context

在Go语言中,context(上下文)是一个标准库包,它提供了一种在goroutine之间传递请求范围的值、取消信号和截止时间的机制。context包的核心是Context接口,它定义了四个方法:

  • Deadline:返回Context被取消的时间,也就是完成工作的截止时间。
  • Done:返回一个<-chan struct{},当Context被取消或者超时时,该通道会被关闭,从而发出信号。
  • Err:返回Context被取消的原因。
  • Value:返回与Context关联的键值对数据。

context包的主要用途包括:

  1. 取消操作:当一个请求被取消或超时时,可以使用context来通知所有处理该请求的goroutine停止工作,从而避免不必要的资源消耗。

  2. 传递请求范围的值:在处理请求的过程中,可能需要在多个goroutine之间共享一些数据,如认证令牌、请求ID等。context提供了一种方便的方式来传递这些数据。

  3. 截止时间管理:在处理请求时,可能需要设置一个截止时间,超过这个时间后,无论工作是否完成,都应该停止工作。context可以用来设置和管理这些截止时间。

使用context的基本模式是:

  • 在处理请求的顶级goroutine中创建一个Context对象,通常是通过调用context.Background()context.TODO()来获取一个空的Context,然后使用context.WithCancelcontext.WithDeadlinecontext.WithTimeoutcontext.WithValue来创建一个具体的Context
  • 将这个Context传递给处理请求的所有goroutine。
  • 在goroutine中,通过检查ContextDone通道来判断是否应该停止工作。
  • 使用ContextValue方法来获取请求范围的值。

例如,以下是一个使用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创建了一个可以取消的Contextcancel函数用于发出取消信号。当cancel被调用时,所有监听ctx.Done()通道的goroutine都会收到信号,从而知道应该停止工作。

Go的内存分布

Go 的内存分布主要包括以下几个区域:

  1. 栈内存(Stack Memory)

    • 每个 Goroutine 都有一个独立的栈内存,初始大小很小(如 2KB),但可以动态增长。
    • 栈内存用于存储局部变量、函数参数和返回值等。
  2. 堆内存(Heap Memory)

    • 动态分配的内存块(通过 newmake 分配)存储在堆上。
    • 堆内存由 Go 的垃圾回收器管理。
  3. 全局/静态内存(Global/Static Memory)

    • 用于存储全局变量和静态变量。
    • 在程序的整个生命周期内都存在。
  4. 文本段(Text Segment)

    • 存储程序的代码,即可执行指令。
  5. 数据段(Data Segment)

    • 存储已初始化的全局变量和静态变量。

Go的GC(垃圾回收)

Go 的垃圾回收器是一个非分代、并发标记清除垃圾回收器。其工作过程如下:

  1. 标记阶段(Marking Phase)

    • 垃圾回收器遍历所有的可达对象并标记它们。
    • 可达对象是指从根对象(全局变量、栈变量、寄存器等)开始,沿着引用链可以访问到的对象。
  2. 清除阶段(Sweeping Phase)

    • 标记阶段结束后,垃圾回收器会清除未标记的对象,并将这些对象的内存归还给堆。

Go 的垃圾回收器是并发的,这意味着它可以与应用程序的 Goroutine 同时运行,以减少垃圾回收带来的停顿时间(STW,Stop-The-World)。

sync.Map是如何实现并发安全的?

sync.Map 是 Go 提供的一个并发安全的 map 实现,主要通过以下方式实现并发安全:

  1. 读写分离

    • 使用两个不同的数据结构(read 和 dirty)来存储数据,read 用于大多数读取操作,dirty 用于写入操作。
    • 读取操作不会锁住整个 map,而是直接访问 read 数据结构。
  2. 原子操作

    • 对于读取操作,使用原子操作来确保并发安全。
    • 当写入操作需要修改 dirty map 时,会使用互斥锁(sync.Mutex)来确保安全。
  3. 惰性初始化

    • 如果 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!
}

在上述代码中,DuckPerson 都实现了 Quacker 接口中的 Quack 方法,因此它们都可以作为 Quacker 类型的参数传递给 makeItQuack 函数。