https://inasa.dev/blog/rss.xml

Go | nocopy机制

2024-03-09

如何保证nocopy

runtime checking

strings.Builder中copy检查

func main() {
    var a strings.Builder
    a.Write([]byte("a"))
    b := a
    b.Write([]byte("b"))
}
// 运行报错:panic: strings: illegal use of non-zero Builder copied by value

源码

type Builder struct {
	addr *Builder // of receiver, to detect copies by value
	buf  []byte
}

func (b *Builder) copyCheck() {
	if b.addr == nil {
		// This hack works around a failing of Go's escape analysis
		// that was causing b to escape and be heap allocated.
		// See issue 23382.
		// TODO: once issue 7921 is fixed, this should be reverted to
		// just "b.addr = b".
		b.addr = (*Builder)(noescape(unsafe.Pointer(b)))
	} else if b.addr != b {
		panic("strings: illegal use of non-zero Builder copied by value")
	}
}

// Write appends the contents of p to b's buffer.
// Write always returns len(p), nil.
func (b *Builder) Write(p []byte) (int, error) {
	b.copyCheck()
	b.buf = append(b.buf, p...)
	return len(p), nil
}

addr是一个指向自身的指针。当a复制给b时,a和b本身是不同的对象。因此,b.addr实际还是指向a的指针,这就会触发条件b.addr!=b,造成panic

sync.Cond中copy检查

type Cond struct {
	noCopy noCopy

	// L is held while observing or changing the condition
	L Locker

	notify  notifyList
	checker copyChecker
}

type copyChecker uintptr

func (c *copyChecker) check() {
	// Check if c has been copied in three steps:
	// 1. The first comparison is the fast-path. If c has been initialized and not copied, this will return immediately. Otherwise, c is either not initialized, or has been copied.
	// 2. Ensure c is initialized. If the CAS succeeds, we're done. If it fails, c was either initialized concurrently and we simply lost the race, or c has been copied.
	// 3. Do step 1 again. Now that c is definitely initialized, if this fails, c was copied.
	if uintptr(*c) != uintptr(unsafe.Pointer(c)) &&
		!atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c))) &&
		uintptr(*c) != uintptr(unsafe.Pointer(c)) {
		panic("sync.Cond is copied")
	}
}

定义一个相似的结构体对象,来探究这里的check函数究竟是如何做copy检查的。

type cond struct {
    checker copyChecker
}

type copyChecker uintptr

func (c *copyChecker) check() {
    fmt.Printf("Before: c: %12v, *c: %12v, uintptr(*c): %12v, uintptr(unsafe.Pointer(c)): %12v\n", c, *c, uintptr(*c), uintptr(unsafe.Pointer(c)))
    swapped := atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c)))
    fmt.Printf("After : c: %12v, *c: %12v, uintptr(*c): %12v, uintptr(unsafe.Pointer(c)): %12v, swapped: %12v\n", c, *c, uintptr(*c), uintptr(unsafe.Pointer(c)), swapped)
}

func main() {
    var a cond
    a.checker.check()
    b := a
    b.checker.check()
}

// 输出
Before: c: 0xc0000b4008, *c:            0, uintptr(*c):            0, uintptr(unsafe.Pointer(c)): 824634458120
After : c: 0xc0000b4008, *c: 824634458120, uintptr(*c): 824634458120, uintptr(unsafe.Pointer(c)): 824634458120, swapped:         true
Before: c: 0xc0000b4040, *c: 824634458120, uintptr(*c): 824634458120, uintptr(unsafe.Pointer(c)): 824634458176
After : c: 0xc0000b4040, *c: 824634458120, uintptr(*c): 824634458120, uintptr(unsafe.Pointer(c)): 824634458176, swapped:        false

当a被bcopy之后,uintptr(*c)uintptr(unsafe.Pointer(c))的值是不同的,通过uint对象的原子比较方法CompareAndSwapUintptr将返回false,它证明了对象a被copy过,从而调用panic保护sync.Cond不被复制。

go vet checking

// noCopy may be added to structs which must not be copied
// after the first use.
//
// See https://golang.org/issues/8005#issuecomment-190753527
// for details.
//
// Note that it must not be embedded, due to the Lock and Unlock methods.
type noCopy struct{}

// Lock is a no-op used by -copylocks checker from `go vet`.
func (*noCopy) Lock()   {}
func (*noCopy) Unlock() {}

runtime时的copy检查虽然很重要,但是,该操作会影响程序的执行性能。

对于其他需要nocopy对象类型来说,使用go vet工具来做静态编译检查。

具体实施来说,就是该对象,或对象中存在filed,它拥有Lock()Unlock()方法,即实现sync.Locker接口。之后,可以通过go vet功能,来检查代码中该对象是否有被copy

静态检查

// wg.go
package main

import "sync"

func main() {
	var sm sync.Mutex
	sm.Lock()
	sm.Unlock()
	sm2 := sm
	sm2.Lock()
}

该代码运行时,不会报错,但是却存在安全隐患。

$ go vet wg.go
 # command-line-arguments
./wg.go:9:9: assignment copies lock value to sm2: sync.Mutex

因此,定义某对象不能被copy,就可以嵌入noCopy结构体,最终通过go vet进行copy检查。

type noCopy struct{}
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
type MyType struct {
   noCopy noCopy
   ...
}