rabbitmq几种路由
简单,work,订阅,路由,主题
简单模式 (Simple Mode ):也称为点对点模式 (Point-to-Point),是最简单的模式。一个生产者发送消息到一个队列,一个消费者从队列中接收并处理消息。消息只会被一个消费者接收和处理,适用于单个消费者场景。
发布/订阅模式 (Publish/Subscribe Mode ):也称为广播模式 (Broadcast),一个生产者发送消息到一个交换机,交换机将消息广播给多个队列。每个队列有一个对应的消费者进行消息的接收和处理。适用于多个消费者同时接收同一份消息的场景。
工作队列模式 (Work Queue Mode ):也称为任务队列模式 (Task Queue),一个生产者发送消息到一个队列,多个消费者并行地从队列中接收和处理消息。消息会被竞争性地分发给多个消费者,每个消息只会被一个消费者接收和处理。适用于任务分发和负载均衡的场景。
主题模式 (Topic Mode ):消息通过路由键 (Routing Key) 的模式匹配来进行订阅和路由。一个生产者发送消息到一个交换机,交换机根据消息的路由键将消息路由到相关的队列。消费者可以使用通配符匹配路由键,选择性地接收和处理消息。适用于灵活的消息订阅和过滤的场景。
头部模式 (Header Mode ):消息通过消息头的匹配来进行订阅和路由。生产者发送带有特定消息头的消息到一个交换机,交换机根据消息头的匹配将消息路由到相关的队列。适用于更复杂的消息匹配和路由场景。
火焰图
y 轴表示调用栈,每一层都是一个函数。调用栈越深,火焰就越高,顶部就是正在执行的函数,下方都是它的父函数。
x 轴表示抽样数,如果一个函数在 x 轴占据的宽度越宽,就表示它被抽到的次数多,即执行的时间长。
注意,x 轴不代表时间,而是所有的调用栈合并后,按字母顺序排列的。
slice 扩容机制
GO1.17 版本及之前
当新切片需要的容量cap大于两倍扩容的容量,则直接按照新切片需要的容量扩容;
当原 slice 容量 < 1024 的时候,新 slice 容量变成原来的 2 倍;
当原 slice 容量 > 1024 ,进入一个循环,每次容量变成原来的1.25 倍,直到大于期望容量。
GO1.18 之后
当新切片需要的容量cap大于两倍扩容的容量,则直接按照新切片需要的容量扩容;
threshold 为 256
当原 slice 容量 < threshold 的时候,新 slice 容量变成原来的 2 倍;
当原 slice 容量 > threshold,进入一个循环,每次容量增加(旧容量+3 *threshold)/4 。
map 底层原理
map 是一个指针 占用8 个字节(64 位计算机),指向hmap结构体,hmap包含多个bmap数组(桶)
type hmap struct {
count int
flags uint8
B uint8
noverflow uint16
hash0 uint32
buckets unsafe.Pointer
oldbuckets unsafe.Pointer
nevacute uintptr
extra *mapextra
}
type bmap struct {
tophash [bucketCnt]uint8
}
type bmap struct {
topbits [8 ]uint8
keys [8 ]keytype
values [8 ]valuetype
pad uintptr
overflow uintptr
}
map 扩容机制
扩容时机:向 map 插入新 key 的时候,会进行条件检测,符合下面这 2 个条件,就会触发扩容
扩容条件:
1. 超过负载 map 元素个数 > 6.5 (负载因子) * 桶个数
2. 溢出桶太多
当桶总数<2 ^15 时,如果溢出桶总数>=桶总数,则认为溢出桶过多
当桶总数>2 ^15 时,如果溢出桶总数>=2 ^15 ,则认为溢出桶过多
扩容机制:
双倍扩容:针对条件1 ,新建一个buckets数组,新的buckets大小是原来的2 倍,然后旧buckets数据搬迁到新的buckets。
等量扩容:针对条件2 ,并不扩大容量,buckets数量维持不变,重新做一遍类似双倍扩容的搬迁动作,把松散的键值对重新排列一次,
使得同一个 bucket 中的 key 排列地更紧密,节省空间,提高 bucket 利用率,进而保证更快的存取。
渐进式扩容:
插入修改删除key的时候,都会尝试进行搬迁桶的工作,每次都会检查oldbucket是否nil ,如果不是nil 则每次搬迁2 个桶,蚂蚁搬家一样渐进式扩容
map 遍历为什么无序
map 每次遍历,都会从一个随机值序号的桶,再从其中随机的cell开始遍历,并且扩容后,原来桶中的key会落到其他桶中,本身就会造成失序.
map 如何查找
1 .写保护机制
先查hmap的标志位flags,如果flags写标志位此时是1 ,说明其他协程正在写操作,直接panic
2 .计算hash值
key 经过哈希函数计算后,得到64 bit(64 位CPU)
10010111 | 101011101010110101010101101010101010 | 10010
3 .找到hash对应的桶
上面64 位后5 (hmap的B值)位定位所存放的桶
如果当前正在扩容中,并且定位到旧桶数据还未完成迁移,则使用旧的桶
4 .遍历桶查找
上面64 位前8 位用来在tophash数组查找快速判断key 是否在当前的桶中,如果不在需要去溢出桶查找
5 .返回key 对应的指针
map 冲突解决方式
GO采用链地址法解决冲突,具体就是插入key 到map中时,当key 定位的桶填满8 个元素后,将会创建一个溢出桶,并且将溢出桶插入当前桶的所在链表尾部
map 负载因子为什么是 6.5
负载因子 = 哈希表存储的元素个数 / 桶个数
Go 官方发现:装载因子越大,填入的元素越多,空间利用率就越高,但发生哈希冲突的几率就变大。
装载因子越小,填入的元素越少,冲突发生的几率减小,但空间浪费也会变得更多,而且还会提高扩容操作的次数
Go 官方取了一个相对适中的值,把 Go 中的 map 的负载因子硬编码为 6.5 ,这就是 6.5 的选择缘由。
这意味着在 Go 语言中,当 map 存储的元素个数大于或等于 6.5 * 桶个数 时,就会触发扩容行为。
Map 和 Sync.Map 哪个性能好
type Map struct {
mu Mutex
read atomic.Value
dirty map [interface()]*entry
misses int
}
对比原始map :
和原始map +RWLock的实现并发的方式相比,减少了加锁对性能的影响。它做了一些优化:可以无锁访问read map ,而且会优先操作read map ,
倘若只操作read map 就可以满足要求,那就不用去操作write map (dirty),所以在某些特定场景中它发生锁竞争的频率会远远小于map +RWLock的实现方式
优点:
适合读多写少的场景
缺点:
写多的场景,会导致 read map 缓存失效,需要加锁,冲突变多,性能急剧下降
Channel 底层实现原理
通过var 声明或者make 函数创建的channel变量是一个存储在函数栈帧上的指针,占用8 个字节,指向堆上的hchan结构体
type hchan struct {
closed uint32
elemtype *_type
buf unsafe.Pointer
qcount uint
dataqsiz uint
elemsize uint16
sendx uint
recvx uint
recvq waitq
sendq waitq
lock mutex
}
等待队列:
双向链表,包含一个头结点和一个尾结点
每个节点是一个sudog结构体变量,记录哪个协程在等待,等待的是哪个channel,等待发送/接收的数据在哪里
type waitq struct {
first *sudog
last *sudog
}
type sudog struct {
g *g
next *sudog
prev *sudog
elem unsafe.Pointer
c *hchan
...
}
创建时:
创建时会做一些检查:
- 元素大小不能超过 64K
- 元素的对齐大小不能超过 maxAlign 也就是 8 字节
- 计算出来的内存是否超过限制
创建时的策略:
- 如果是无缓冲的 channel,会直接给 hchan 分配内存
- 如果是有缓冲的 channel,并且元素不包含指针,那么会为 hchan 和底层数组分配一段连续的地址
- 如果是有缓冲的 channel,并且元素包含指针,那么会为 hchan 和底层数组分别分配地址
发送时:
- 如果 channel 的读等待队列存在接收者goroutine
- 将数据直接发送给第一个等待的 goroutine,唤醒接收的 goroutine
- 如果 channel 的读等待队列不存在接收者goroutine
- 如果循环数组buf未满,那么将会把数据发送到循环数组buf的队尾
- 如果循环数组buf已满,这个时候就会走阻塞发送的流程,将当前 goroutine 加入写等待队列,并挂起等待唤醒
接收时:
- 如果 channel 的写等待队列存在发送者goroutine
- 如果是无缓冲 channel,直接从第一个发送者goroutine那里把数据拷贝给接收变量,唤醒发送的 goroutine
- 如果是有缓冲 channel(已满),将循环数组buf的队首元素拷贝给接收变量,将第一个发送者goroutine的数据
拷贝到 buf循环数组队尾,唤醒发送的 goroutine
- 如果 channel 的写等待队列不存在发送者goroutine
- 如果循环数组buf非空,将循环数组buf的队首元素拷贝给接收变量
- 如果循环数组buf为空,这个时候就会走阻塞接收的流程,将当前 goroutine 加入读等待队列,并挂起等待唤醒
Channel 有什么特点
channel有2 种类型:无缓冲、有缓冲
channel有3 种模式:写操作模式(单向通道)、读操作模式(单向通道)、读写操作模式(双向通道)
写操作模式 make (chan <- int )
读操作模式 make (<-chan int )
读写操作模式 make (chan int )
channel 有 3 种状态:未初始化、正常、关闭
操作 未初始化 关闭 正常
关闭 panic panic 正常
发送 永远阻塞导致死锁 panic 阻塞或者成功发送
接收 永远阻塞导致死锁 缓冲区为空则为零值,否则可以继续读 阻塞或者成功接收
注意点:
一个 channel不能多次关闭,会导致painc
如果多个 goroutine 都监听同一个 channel,那么 channel 上的数据都可能随机被某一个 goroutine 取走进行消费
如果多个 goroutine 监听同一个 channel,如果这个 channel 被关闭,则所有 goroutine 都能收到退出信号
Channel 为什么是线程安全的
不同协程通过channel进行通信,本身的使用场景就是多线程,为了保证数据的一致性,必须实现线程安全
channel的底层实现中,hchan结构体中采用Mutex锁来保证数据读写安全。在对循环数组buf中的数据进行入队和出队操作时,
必须先获取互斥锁,才能操作channel数据
Channel 发送和接收什么情况下会死锁
func deadlock1 () {
ch := make (chan int )
ch <- 3
}
func deadlock2 () {
ch := make (chan int )
ch <- 3
num := <-ch
fmt.Println("num=" , num)
}
func deadlock3 () {
ch := make (chan int )
ch <- 100
go func () {
num := <-ch
fmt.Println("num=" , num)
}()
time.Sleep(time.Second)
}
func deadlock3 () {
ch := make (chan int , 3 )
ch <- 3
ch <- 4
ch <- 5
ch <- 6
}
func deadlock4 () {
ch := make (chan int )
fmt.Println(<-ch)
}
func deadlock5 () {
ch1 := make (chan int )
ch2 := make (chan int )
go func () {
for {
select {
case num := <-ch1:
fmt.Println("num=" , num)
ch2 <- 100
}
}
}()
for {
select {
case num := <-ch2:
fmt.Println("num=" , num)
ch1 <- 300
}
}
}
互斥锁实现原理
Go sync 包提供了两种锁类型:互斥锁sync .Mutex 和 读写互斥锁sync .RWMutex,都属于悲观锁。
锁的实现一般会依赖于原子操作、信号量,通过atomic 包中的一些原子操作来实现锁的锁定,通过信号量来实现线程的阻塞与唤醒
在正常模式下,锁的等待者会按照先进先出的顺序获取锁。但是刚被唤起的 Goroutine 与新创建的 Goroutine 竞争时,大概率会
获取不到锁,在这种情况下,这个被唤醒的 Goroutine 会加入到等待队列的前面。 如果一个等待的 Goroutine 超过1 ms 没有获取锁,
那么它将会把锁转变为饥饿模式。
Go在1.9 中引入优化,目的保证互斥锁的公平性。在饥饿模式中,互斥锁会直接交给等待队列最前面的 Goroutine。新的 Goroutine
在该状态下不能获取锁、也不会进入自旋状态,它们只会在队列的末尾等待。如果一个 Goroutine 获得了互斥锁并且它在队列的末尾或者
它等待的时间少于 1 ms,那么当前的互斥锁就会切换回正常模式。
互斥锁允许自旋的条件?
线程没有获取到锁时常见有2 种处理方式:
- 一种是没有获取到锁的线程就一直循环等待判断该资源是否已经释放锁,这种锁也叫做自旋锁,它不用将线程阻塞起来,
适用于并发低且程序执行时间短的场景,缺点是cpu 占用较高
- 另外一种处理方式就是把自己阻塞起来,会释放CPU 给其他线程,内核会将线程置为「睡眠」状态,等到锁被释放后,
内核会在合适的时机唤醒该线程,适用于高并发场景,缺点是有线程上下文切换的开销
Go语言中的Mutex实现了自旋与阻塞两种场景,当满足不了自旋条件时,就会进入阻塞
**允许自旋的条件:**
1 . 锁已被占用,并且锁不处于饥饿模式。
2 . 积累的自旋次数小于最大自旋次数(active_spin=4 )。
3 . cpu 核数大于 1 。
4 . 有空闲的 P。
5 . 当前 goroutine 所挂载的 P 下,本地待运行队列为空。
读写锁实现原理
读写锁的底层是基于互斥锁实现的。
写锁需要阻塞写锁:一个协程拥有写锁时,其他协程写锁定需要阻塞;
写锁需要阻塞读锁:一个协程拥有写锁时,其他协程读锁定需要阻塞;
读锁需要阻塞写锁:一个协程拥有读锁时,其他协程写锁定需要阻塞;
读锁不能阻塞读锁:一个协程拥有读锁时,其他协程也可以拥有读锁。
原子操作有哪些
Go atomic包是最轻量级的锁(也称无锁结构),可以在不形成临界区和创建互斥量的情况下完成并发安全的值替换操作,
不过这个包只支持int32/int64/uint32/uint64/uintptr这几种数据类型的一些基础操作(增减、交换、载入、存储等)
当我们想要对**某个变量** 并发安全的修改,除了使用官方提供的 `mutex` ,还可以使用 sync/atomic 包的原子操作,
它能够保证对变量的读取或修改期间不被其他的协程所影响。
atomic 包提供的原子操作能够确保任一时刻只有一个goroutine对变量进行操作,善用 atomic 能够避免程序中出现大量的锁操作。
**常见操作:**
- 增减Add AddInt32 AddInt64 AddUint32 AddUint64 AddUintptr
- 载入Load LoadInt32 LoadInt64 LoadPointer LoadUint32 LoadUint64 LoadUintptr
- 比较并交换CompareAndSwap CompareAndSwapInt32...
- 交换Swap SwapInt32...
- 存储Store StoreInt32...
原子操作和锁的区别
原子操作由底层硬件支持,而锁是基于原子操作+信号量完成的。若实现相同的功能,前者通常会更有效率
原子操作是单个指令的互斥操作;互斥锁/读写锁是一种数据结构,可以完成临界区(多个指令)的互斥操作,扩大原子操作的范围
原子操作是无锁操作,属于乐观锁;说起锁的时候,一般属于悲观锁
原子操作存在于各个指令/语言层级,比如“机器指令层级的原子操作”,“汇编指令层级的原子操作”,“Go语言层级的原子操作”等。
锁也存在于各个指令/语言层级中,比如“机器指令层级的锁”,“汇编指令层级的锁”,“Go语言层级的锁”等
goroutine 的底层实现原理
g本质是一个数据结构,真正让 goroutine 运行起来的是调度器
type g struct {
goid int64
sched gobuf
stack stack
gopc
startpc uintptr
}
type gobuf struct {
sp uintptr
pc uintptr
g guintptr
ret uintptr
}
type stack struct {
lo uintptr
hi uintptr
}
goroutine 和线程的区别
内存占用:
创建一个 goroutine 的栈内存消耗为 2 KB,实际运行过程中,如果栈空间不够用,会自动进行扩容。创建一个 thread 则需要消耗 1 MB 栈内存。
创建和销毀:
Thread 创建和销毀需要陷入内核,系统调用。而 goroutine 因为是由 Go runtime 负责管理的,创建和销毁的消耗非常小,是用户级。
切换:
当 threads 切换时,需要保存各种寄存器,而 goroutines 切换只需保存三个寄存器:Program Counter, Stack Pointer and BP。一般而言,
线程切换会消耗 1000-1500 ns,Goroutine 的切换约为 200 ns,因此,goroutines 切换成本比 threads 要小得多。
goroutine 泄露场景
泄露原因
Goroutine 内进行channel/mutex 等读写操作被一直阻塞。
Goroutine 内的业务逻辑进入死循环,资源一直无法释放。
Goroutine 内的业务逻辑进入长时间等待,有不断新增的 Goroutine 进入等待
泄露场景
channel 如果忘记初始化,那么无论你是读,还是写操作,都会造成阻塞。
channel 发送数量 超过 channel接收数量,就会造成阻塞
channel 接收数量 超过 channel发送数量,也会造成阻塞
http request body未关闭,goroutine不会退出
互斥锁忘记解锁
sync.WaitGroup使用不当
如何排查
单个函数:调用 `runtime.NumGoroutine` 方法来打印 执行代码前后Goroutine 的运行数量,进行前后比较,就能知道有没有泄露了。
生产/测试环境:使用`PProf` 实时监测Goroutine的数量
如何查看正在运行的 goroutine 数量
package main
import (
"net/http"
_ "net/http/pprof"
)
func main () {
for i := 0 ; i < 100 ; i++ {
go func () {
select {}
}()
}
go func () {
http.ListenAndServe("localhost:6060" , nil )
}()
select {}
}
执行程序之后,命令运行以下命令,会自动打开浏览器显示一系列目前还看不懂的图,
提示Could not execute dot; may need to install graphviz.则需要安装graphviz,需要python环境
go tool pprof -http=:1248 http:
如何控制并发的 goroutine 数量?
在开发过程中,如果不对goroutine加以控制而进行滥用的话,可能会导致服务整体崩溃。比如耗尽系统资源导致程序崩溃,或者CPU使用率过高导致系统忙不过来。
解决方案:
有缓冲channel:利用缓冲满时发送阻塞的特性
无缓冲channel:任务发送和执行分离,指定消费者并发协程数
GMP 和 GM 模型
G:Goroutine
M: 线程
P: Processor 本地队列
GMP 模型:
P的数量:
由启动时环境变量`$GOMAXPROCS`或者是由`runtime`的方法`GOMAXPROCS()`决定
M的数量:
go 语言本身的限制:go程序启动时,会设置M的最大数量,默认10000 .但是内核很难支持这么多的线程数
runtime /proc 中的sched.maxmcount,设置M的最大数量
一个M阻塞了,会创建新的M。
P何时创建:在确定了P的最大数量n后,运行时系统会根据这个数量创建n个P。
M何时创建:没有足够的M来关联P并运行其中的可运行的G。比如所有的M此时都阻塞住了,而P中还有很多就绪任务,就会去寻找空闲的M,而没有空闲的,就会去创建新的M。
全场景解析:
1 .P拥有G1,M1获取P后开始运行G1,G1创建了G2,为了局部性G2优先加入到P1 的本地队列。
2 .G1运行完成后,M上运行的goroutine切换为G0,G0负责调度时协程的切换。从P的本地队列取G2,从G0切换到G2,并开始运行G2。实现了线程M1的复用。
3 .假设每个P的本地队列只能存4 个G。G2要创建了6 个G,前4 个G(G3, G4, G5, G6)已经加入p1 的本地队列,p1 本地队列满了。
4 .G2在创建G7的时候,发现P1 的本地队列已满,需要执行负载均衡(把P1 中本地队列中前一半的G,还有新创建G转移到全局队列),这些G被转移到全局队列时,会被打乱顺序
5 .G2创建G8时,P1 的本地队列未满,所以G8会被加入到P1 的本地队列。
6 .在创建G时,运行的G会尝试唤醒其他空闲的P和M组合去执行。假定G2唤醒了M2,M2绑定了P2 ,并运行G0,但P2 本地队列没有G,M2此时为自旋线程
7 .M2尝试从全局队列取一批G放到P2 的本地队列,至少从全局队列取1 个g,但每次不要从全局队列移动太多的g到p本地队列,给其他p留点。
8 .假设G2一直在M1上运行,经过2 轮后,M2已经把G7、G4从全局队列获取到了P2 的本地队列并完成运行,全局队列和P2 的本地队列都空了,那m就要执行work stealing(偷取):
从其他有G的P哪里偷取一半G过来,放到自己的P本地队列。P2 从P1 的本地队列尾部取一半的G
9 .G1本地队列G5、G6已经被其他M偷走并运行完成,当前M1和M2分别在运行G2和G8,M3和M4没有goroutine可以运行,M3和M4处于自旋状态,
它们不断寻找goroutine。系统中最多有GOMAXPROCS个自旋的线程,多余的没事做线程会让他们休眠。
10 .假定当前除了M3和M4为自旋线程,还有M5和M6为空闲的线程,G8创建了G9,G8进行了阻塞的系统调用,M2和P2 立即解绑,P2 会执行以下判断:
如果P2 本地队列有G、全局队列有G或有空闲的M,P2 都会立马唤醒1 个M和它绑定,否则P2 则会加入到空闲P列表,等待M来获取可用的p。
11 .G8创建了G9,假如G8进行了非阻塞系统调用。M2和P2 会解绑,但M2会记住P2 ,然后G8和M2进入系统调用状态。当G8和M2退出系统调用时,
会尝试获取P2 ,如果无法获取,则获取空闲的P,如果依然没有,G8会被记为可运行状态,并加入到全局队列,M2因为没有P的绑定而变成休眠状态
work stealing 机制?
当线程M⽆可运⾏的G时,尝试从其他M绑定的P偷取G,减少空转,提高了线程利用率(避免闲着不干活)。
当从本线程绑定 P 本地 队列、全局G队列、netpoller都找不到可执行的 g,会从别的 P 里窃取G并放到当前P上面。
从netpoller 中拿到的G是_Gwaiting状态( 存放的是因为网络IO被阻塞的G),从其它地方拿到的G是_Grunnable状态
从全局队列取的G数量:N = min (len (GRQ)/GOMAXPROCS + 1 , len (GRQ/2 )) (根据GOMAXPROCS负载均衡)
从其它P本地队列窃取的G数量:N = len (LRQ)/2 (平分)
GC 如何调优
1. 控制内存分配的速度,限制 Goroutine 的数量,提高赋值器 mutator 的 CPU 利用率(降低GC的CPU利用率)
2. 少使用+连接string
3. slice提前分配足够的内存来降低扩容带来的拷贝
4. 避免map key对象过多,导致扫描时间增加
5. 变量复用,减少对象分配,例如使用 sync.Pool 来复用需要频繁创建临时对象、使用全局变量等
6. 增大 GOGC 的值,降低 GC 的运行频率 (不太用这个)
select的数据结构
type scase struct {
c *hchan
elem unsafe.Pointer
}
新版本使用
type selectDir int
const (
_ selectDir = iota
selectSend
selectRecv
selectDefault
)
go锁的正常模式和饥饿模式?
Go语言中的互斥锁(Mutex)有两种模式:正常模式和饥饿模式。
**正常模式** :
- 在正常模式下,所有等待锁的goroutine按照FIFO(先进先出)顺序排队。当一个goroutine被唤醒时,它不会直接获得锁,
而是需要与新请求锁的goroutine竞争。由于新请求的goroutine已经在CPU上运行,它们有优势,因此被唤醒的goroutine很可能在竞争中失败,从而需要重新排队。
**饥饿模式** :
- 饥饿模式是为了解决长时间等待的goroutine可能永远无法获得锁的问题。当一个goroutine等待锁的时间超过1毫秒时,
互斥锁会切换到饥饿模式。在饥饿模式下,锁的所有权直接从解锁的goroutine移交给等待队列头部的goroutine。新来的goroutine不会参与抢锁,
而是直接进入等待队列的尾部,这样保证了老的goroutine能够获得锁。
三色标记法
### 三色标记法的基本概念
1. **白色对象** :尚未被垃圾回收器访问的对象,表示这些对象可能不可达,是潜在的垃圾对象。
2. **灰色对象** :已被垃圾回收器访问,但回收器还需要对其引用的一个或多个对象进行扫描,因为它们可能还指向白色对象。
3. **黑色对象** :已被垃圾回收器完全访问并扫描过,其中所有字段都已被检查,黑色对象中的任何一个指针都不可能直接指
向白色对象,它们被认为是活跃的对象,不需要被回收。
### 三色标记法的工作流程
1. **初始化阶段** :所有对象初始时都被标记为白色。
2. **标记阶段** :从根对象开始,逐步访问所有可达对象,将它们标记为灰色。
3. **重新标记阶段** :将灰色对象的子对象标记为灰色,并将灰色对象自身标记为黑色。
4. **清理阶段** :遍历所有白色对象,这些对象被认为是不可达的,可以被回收。
### Go语言中的三色标记法实现
Go语言的垃圾回收器采用了三色标记法,并对其进行了优化以适应其并发执行的特性。
- **并发标记** :Go的垃圾回收器可以在程序继续运行的同时进行标记,减少了程序的暂停时间。
- **写屏障** :为了处理并发标记中可能出现的竞争条件,Go使用写屏障技术来确保对象颜色的正确性。
- **增量回收** :Go语言的垃圾回收是增量进行的,这意味着它被分成多个小步骤,以减少单次回收的停顿时间。
### 三色不变性
在并发和增量标记算法中,需要满足黑色对象不会指向白色对象的三色不变性,以确保标记的正确性。这包括强三色不变式和弱三色不变式两种形式:
- **强三色不变式** :不允许黑色对象引用白色对象,以防止白色对象被错误地回收。
- **弱三色不变式** :黑色对象可以引用白色对象,但要求该白色对象必须存在其他灰色对象对它的引用,或者可达它的链路上游存在灰色对象。
通过这些机制,Go语言的垃圾回收器能够在保持高效率的同时,准确地识别和回收不再使用的对象。
什么是m0 g0
在Go语言中,`m0` 和`g0` 是两个与Go运行时调度器密切相关的概念:
1. **m0** :
- `m0` 是Go运行时创建的第一个系统线程,也称为主线程。一个Go进程只有一个`m0` 。
- 在数据结构上,`m0` 与其他创建的`m` (线程)没有任何区别。
- `m0` 的创建过程是在进程启动时由汇编直接复制给`m0` 的,而其他后续的`m` 则是由Go运行时内部自行创建的。
- `m0` 的变量声明与常规`m` 一样,定义为`var m0 m` ,没有特别之处。
2. **g0** :
- `g0` 是每个线程(`m` )创建时首先创建的goroutine,每个`m` 都只有一个`g0` ,且每个`m` 都只会绑定一个`g0` 。
- 在数据结构上,`g0` 与其他创建的`g` (goroutine)是一样的,但是存在栈的差别。`g0` 上的栈分配的是系统栈,在Linux上栈大小默认固定8MB,
不能扩缩容。而常规的`g`起始只有2KB,可扩容。
- `g0`的运行状态与常规的`g`不一样,它没有那么多运行状态,也不会被调度程序抢占,因为调度本身就是在`g0`上运行的。
- `g0`的变量声明与常规`g`一样,定义为`var g0 g`,没有特别之处。
简而言之,`m0` 是Go程序中的第一个系统线程,而`g0` 是每个线程中用于调度的特殊的goroutine
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具