type Pool struct {
noCopy noCopy
local unsafe.Pointer // P 本地池,固定尺寸,实际结构 [P]poolLocal,类似 void* 并附加长度构成了一个数组
localSize uintptr // size of the local array
victim unsafe.Pointer // local from previous cycle
victimSize uintptr // size of victims array
New func() any
}
type poolChain struct{
head *poolChainElt
tail *poolChainElt
}
type poolChainElt struct{ // 一个双向链表
poolDequeue
next,prev *poolChainElt
}
type poolDequeue struct{
headtail uint64
vals []eface
}
type eface struct{ // 数据的真实内存分配,包括一个类型描述和实际数据
typ,val unsafe.Pointer
}
type poolLocalInternal struct{
private any // p的私有缓冲区
shared poolChain // 公共缓冲区
}
type poolLocal struct{
poolLocalInternal
pad [128-unsafe.Sizeof(poolLocalInternal{})%128]byte // 应该是补位,可以确保刚好占满一个 cache line 加速访问
}
func (p *Pool) pin() (*poolLocal, int) {
pid := runtime_procPin() // 将当前G和P绑定,并返回P的id
s := runtime_LoadAcquintptr(&p.localSize) // load-acquire
l := p.local // load-consume
if uintptr(pid) < s { // 主要是P的数量可能会变动 重新找一个
return indexLocal(l, pid), pid
}
return p.pinSlow()
}
func (p *Pool) pinSlow() (*poolLocal, int) {
runtime_procUnpin() // 禁止 P 抢占,否则当前G会被放回本地或者全局队列,当时之后G不一定在现在这个P上
allPoolsMu.Lock()
defer allPoolsMu.Unlock()
pid := runtime_procPin()
s := p.localSize
l := p.local
if uintptr(pid) < s { // double check
return indexLocal(l, pid), pid
}
if p.local == nil { // 如果本地队列为空,那么此时Pool没被初始化,加入全局池引用
allPools = append(allPools, p)
}
size := runtime.GOMAXPROCS(0) // 查看现在P的个数
local := make([]poolLocal, size) // 为这个Pool分配跟P数量相同的本地池
atomic.StorePointer(&p.local, unsafe.Pointer(&local[0])) // store-release
runtime_StoreReluintptr(&p.localSize, uintptr(size)) // store-release
return &local[pid], pid // 返回当前和P绑定的本地池
}
func (p *Pool) Get() any {
if race.Enabled {
race.Disable()
}
l, pid := p.pin() // 先找本地池
x := l.private
l.private = nil
if x == nil { // 如果没有,那么从全局池找
// Try to pop the head of the local shard. We prefer
// the head over the tail for temporal locality of
// reuse.
x, _ = l.shared.popHead()
if x == nil {
x = p.getSlow(pid)
}
}
runtime_procUnpin() //释放P,让其可以被抢占
if race.Enabled {
race.Enable()
if x != nil {
race.Acquire(poolRaceAddr(x))
}
}
if x == nil && p.New != nil {
x = p.New()
}
return x
}
// Put adds x to the pool.
func (p *Pool) Put(x any) {
if x == nil {
return
}
if race.Enabled {
if fastrandn(4) == 0 {
// Randomly drop x on floor.
return
}
race.ReleaseMerge(poolRaceAddr(x))
race.Disable()
}
l, _ := p.pin() // 老规矩,先禁止P被抢占
if l.private == nil { // 本地没有 则先放入本地
l.private = x
} else {
l.shared.pushHead(x) // 否则放入全局
}
runtime_procUnpin()
if race.Enabled {
race.Enable()
}
}
其实很好理解,正好是一次二级缓冲模型,第一次gc会将local放入 victim,第二gc victim不为空才会真正清理,local不会参与gc
要求系统安装C/C++工具链,macos和linux(gcc 自带),windows(mingw),并确保环境变量
CGO_ENAVBLE=on
,最后单个源码需要导入import "C"
C type | Cgo type | Go type |
---|---|---|
char | C.char | byte |
signed char | C.schar | int8 |
unsigned char | C.uchar | uint8 |
short | C.short | int16 |
unsigned short | C.ushort | uint16 |
int | C.int | int32 |
unsigned int | C.uint | uint32 |
long | C.long | int32 |
unsigned long | C.ulong | uint32 |
long long int | C.longlong | int64 |
unsigned long long int | C.ulonglong | uint64 |
float | C.float | float32 |
double | C.double | double |
size_t | C.size_t | uint |
go引用c的函数指针比较特别
官方给出的Example
GODEBUG=gctrace=1 go run *.go
其中 gctrace=1
表示只针对这个进程进行GC追踪
go采用三色标记法,主要是为了提高并发度,这样扫描过程可以拆分为多个阶段,而不用一次扫描全部
黑 根节点扫描完毕,子节点也扫描完毕
灰 根节点扫描完毕,子节点未扫描
白 未扫描
扫描是从 .bss .data goroutine栈开始扫描,最终遍历整个堆上的对象树
标记过程是一个广度优先的遍历过程,扫描节点,将节点的子节点推送到任务队列中,然后递归扫描子叶节点,直到所有工作队列被排空
mark阶段会将白色对象标记,并推入队列中变为灰色
保障了代码描述中对内存的操作顺序,
即不会在编译期被编译器进行调整,也不会在运行时被CPU的乱序执行所打乱
在应用进入 GC 标记阶段前的 stw 阶段,会将全局变量 runtime.writeBarrier.enabled 修改为 true,这时所有的堆上指针修改操作在修改之前便会额外调用 runtime.gcWriteBarrier
由于GC和Go主程序并发执行,所以必须要在扫描时监控内存可能出现的状态改变,所以需要写屏障,所以需要暂停GO主程序(STW)
改方式的基本思想是:对正在被覆盖的对象进行着色,且如果当时栈未扫描完成,则同样对指针进行着色
程序启动会为每个P分配一个 mark worker 来标记内存,负责为进入STW做前期工作
GC会将灰色object标记为黑色,将灰色object所包含的所有指针所指向的地址都标记为灰色,递归这两个步骤,最终对象非黑即白,其中白色object即未被引用且可以被回收,如果object标记为no scan,则递归结束,标记为黑色
todo https://blog.csdn.net/asd1126163471/article/details/124113816
Golang 默认指针是类型安全的,但它有很多限制。Golang 还有非类型安全的指针,这就是 unsafe 包提供的 unsafe.Pointer。在某些情况下,它会使代码更高效,当然,也更危险。unsafe 包用于 Go 编译器,在编译阶段使用。从名字就可以看出来,它是不安全的,官方并不建议使用。Go 语言类型系统是为了安全和效率设计的,有时,安全会导致效率低下。unsafe 包绕过了 Go 的类型系统,达到直接操作内存的目的,使用它有一定的风险性。但是在某些场景下,使用 unsafe 包提供的函数会提升代码的效率,Go 源码中也是大量使用 unsafe 包。
unsafe
包//定义
type ArbitraryType int
type Pointer *ArbitraryType
//函数
func Sizeof(x AribitraryType) uintptr{}
func Offsetof(x AribitraryType) uintptr{}
func Alignof(x AribitraryType) uintptr{}
Pointer
: 指向任意类型,类似于 C 中的 void*
。
Sizeof
: 返回所传类型的大小,指针只返回指针的本身(x64 8byte x86 4byte
),而不会返回所指向的内存大小。
Offsetof
: 返回 struct
成员在内存中的位置,相对于此结构体的头位置,所传参数必须是结构体成员。传入指针,或者结构体本身,会 error
Alignof
: 返回 M,M 是内存对齐时的倍数。
任意指针都可以和 unsafe.Pointer
相互转换。
uintptr
可以和 unsafe.Pointer
相互转换。
综上,
unsafe.Pointer
是不能进行指针运算的,只能先转为uintptr
计算完再转回unsafe.Pointer
,还有一点要注意的是,uintptr
并没有指针的语义,意思就是uintptr
所指向的对象会被 gc。而unsafe.Pointer
有指针语义,可以保护它所指向的对象在“有用”的时候不会被垃圾回收。
一个 os 线程会有一个给固定大小的内存块(一般是 2MB),用来存储当前线程中调用或挂起函数的内部变量,固定大小的栈对于复杂和深层次递归是不够的,而 Goroutine 会以一个很小的栈(2KB)开始其生命周期,这个栈会动态伸缩,最大能到达 1GB(32位系统是 250M)
os 线程由操作系统内核调用,每过一定时间(毫秒),硬件计时器会中断处理器,并调用一个名为 scheduler 的内建函数,这个函数会挂起当前执行的线程并保存内存中它的寄存器内存,然后检查线程列表并决定下一次执行哪个线程,并从内存中恢复该线程的寄存器信息,恢复该线程的线程并执行,这就是上下文切换,增加了 CPU 的运行周期。而 Go 的 runtime 包含了自身的调度器,和 os 线程不同是,
Goroutine
属于用户级线程由语言支持,调度由语言支持,所有开销会减少很多(相比于内核上下文切换)。