go进阶编程:runtime.Pinner

246 阅读3分钟

深入解析 runtime.Pinner

在 Go 语言的浩瀚宇宙中,每一次版本的更新都如同星辰闪耀,为开发者们带来了无限的可能与惊喜。今天,就让我们一同揭开runtime.Pinner(或相关机制,因为runtime.Pinner并非直接公开的 API)的神秘面纱,探讨它在并发编程中的新角色与区别。

runtime.Pinner:幕后英雄的新篇章

虽然runtime.Pinner这个名字可能并不为大多数开发者所熟知,但它在 Go 运行时的内存管理中扮演着举足轻重的角色。简而言之,runtime.Pinner(或其内部实现)负责将特定的内存页锁定在物理内存中,以防止它们被操作系统交换到磁盘上。这一机制对于需要低延迟、高稳定性的并发应用至关重要。

runtime.Pinner

Pin 固定一个 Go 对象,防止它被垃圾移动或释放收集直到 Pinner.Unpin 方法已被调用。指向固定对象的指针可以直接存储在 C 内存中包含在 Go 内存中,传递给 C 函数。如果是固定对象本身包含指向 Go 对象的指针,这些对象必须单独固定将从 C 代码中访问参数必须是任何类型的指针或[unsafe.Pointer]。在非 go 指针上调用 Pin 是安全的,在这种情况下 Pin 不会做任何事情。

例子 1

Go 侧预先申请好内存,C 侧完成内存赋值

/*
void get_string(void *buf) {
  GoSlice* slice = (GoSlice*)(buf);
  // 在 C 侧完成内存赋值
  memcpy(slice->data, ...);
}
*/
func getStringFromC() {
  buf := make([]byte, len)
  ptr := unsafe.Pointer(&buf)
  sHeader := (*reflect.SliceHeader)(ptr)

  // 将 slice 中的 data 指针指向的对象 Pin 住
  var pinner runtime.Pinner
  defer pinner.Unpin()
  pinner.Pin(unsafe.Pointer(sHeader.Data))

  // 可以将 slice 对象的指针,安全传给 C 了
  C.get_string(ptr)
}

例子 2

类似于 sync.Pool 源码中的 pin,Unpin 操作。

runtime.Pinner

package main
import (
    "fmt"
    "runtime"
)
func main() {
    // 创建一个新的 `[]byte` 数组
    data := make([]byte, 10)
    // 将数组固定
    p := runtime.Pinner.Pin(data)
    // 对数组进行一些操作
    for i := range data {
        data[i] = byte(i)
    }
    // 取消固定数组
    p.Unpin()
    // 打印数组内容
    fmt.Println(data)
}

sync.Pool 部分源码(go 1.22.5)

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()
	if race.Enabled {
		race.Enable()
		if x != nil {
			race.Acquire(poolRaceAddr(x))
		}
	}
	if x == nil && p.New != nil {
		x = p.New()
	}
	return x
}

对并发编程的深远影响

虽然runtime.Pinner的变化对大多数开发者来说是隐形的,但它们对并发编程的影响却是深远的。这些优化使得 Go 程序在处理高并发、低延迟任务时更加得心应手。开发者可以更加专注于业务逻辑的实现,而不必担心底层内存管理的复杂性。

注意

只能对以下类型的对象调用 Pinner.Pin 函数:

  • 通过 new 函数创建的对象
  • 复合字面量的地址
  • 局部变量的地址
  • 如果对非法的对象调用 Pinner.Pin 函数,会导致程序崩溃。
  • 在调用 Pinner.Unpin 函数之前,必须确保不再使用该对象,否则会导致程序运行时错误

结语

通过深入优化runtime.Pinner(或其内部机制)等关键组件,Go 语言为开发者们提供了更加强大、稳定的并发编程环境。以上就是 runtime.Pinner。

引用

欢迎关注我的公众号

"彼岸流天"