如何理解Golang的 “must not be copied after first use”(源码解析)

· · 794 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

前言

阅读Golang sync包时,总会看到一句话“must not be copied after first use”,对此感到很好奇,查阅过程中发现这篇文章总结得挺到位的,因此转载,记录一下,因为我只是对于原理上面好奇,因此没有全文翻译过来,只挑选了一些自己感兴趣的地方用自己的话总结了一下,感兴趣的可以看看原文章:
What does “nocopy after first use” mean in golang and how

正文

must not be copied after first use

初次使用后不能复制,sync包大多跟并发控制相关,出于安全考虑(避免指针的复制使得指针污染不安全,误操作而使程序崩溃)不能复制可以理解,但Golang是怎么样办到的呢,接下来就从源码层面看看

1. 运行时检测,实例地址值传递

这个是在初次时候后记录变量地址,二次使用时比对变量地址,如果不同的话说明被复制了。
首先,我们先来看一个比较明显的例子strings.Builder

type Builder struct {
    addr *Builder     // 关键所在,专门用来记录Builder实例的地址
    buf []byte
}
func (b *Builder) copyCheck() {
    if b.addr == nil {
        // 初始化,记录b实例的地址
        b.addr = (*Builder)(noescape(unsafe.Pointer(b)))
    } else if b.addr != b {
        panic("strings: illegal use of non-zero Builder copied by value")
    }
}
func (b *Builder) Write(p []byte) (int, error) {
    b.copyCheck()
    ...
}
    
// test case
var a strings.Builder
a.Write([]byte("testa"))
var b = a
b.Write([]byte("testb"))   // 这里是复制后使用,所以会诱发panic

很明显,strings.Builder通过一个指针来存储实例化后的实例地址,由于这个值是由内部赋值的,所以初次使用时为 nil,此时会存储地址,下次使用的时候会进行比对,不一致,说明被复制过了

接下来,我们回到sync包,来看看sync.Cond怎么处理

type Cond struct {
    noCopy  noCopy
    L       Locker
    notify  notifyList
    checker copyChecker
}
type copyChecker uintptr
func (c *copyChecker) check() {
    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")
    }
}
func (c *Cond) Wait() {
    c.checker.check()
    ...
}

这里跟strings.Builder有点不一样,因为sync.Cond通过一个结构体copyChecker来进行判断处理,咱们来看看关键代码check()

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")
    }

我们假设一下创建了一个cond,cond := sync.NewCond(new(sync.Mutex)),此时假设内存如 "cond内存示例假设图" 第一部分所示。接下来再调用cond.Wait()后会触发check()里面的

!atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c)))

由于此时checker值为0,因此会把checker的地址存储进去,那么此时cond.checker的值为cond.checker的地址(假设是0x04)

接下来当复制该变量condB := cond时,整块空间会被复制到一个新内存(假设此时checker地址为0x0A),这个时候如果再次调用cond.Wait(),那么一比对就会发现cond被复制了,于是乎就起到了复制检测的功能

cond内存示例假设图

2. 静态代码检测,通过go vet

-copylocks是go vet的一个flag,用来开启是否有不允许拷贝但被拷贝的代码检测,只需要定义一个结构体noCopy,然后嵌入到你不允许拷贝的结构体。如果你希望自己定义的一个结构体使用者无法拷贝,只能指针传递保证全局唯一的话,也可以使用这个方法处理

// noCopy may be embedded into structs which must not be copied
// after the first use.
type noCopy struct{}

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

实例代码:

// file: test.go
package main
    
type noCopy struct{}
    
func (*noCopy) Lock()   {}
func (*noCopy) Unlock() {}
    
// sync.Pool
type Pool struct {
    noCopy noCopy
    val    int
}
    
func main() {
    poolA := Pool{}
    poolB := poolA
    poolB.val = 1024
}

然后通过命令go vet -copylocks ./test.go就可以检测到错误

$ go vet -copylocks ./test.go
# command-line-arguments
.\test.go:16:11: assignment copies lock value to poolB: command-line-arguments.Pool contains command-line-arguments.noCopy

回到sync包,我们也能看到一样的身影

type Cond struct {
    noCopy noCopy
    
    // L is held while observing or changing the condition
    L Locker
  
    notify  notifyList
    checker copyChecker
}

type Pool struct {
    noCopy noCopy
 
    local     unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
    localSize uintptr        // size of the local array
 
    victim     unsafe.Pointer // local from previous cycle
    victimSize uintptr        // size of victims array
 
    // New optionally specifies a function to generate
    // a value when Get would otherwise return nil.
    // It may not be changed concurrently with calls to Get.
    New func() interface{}
}
 
...
 

有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

794 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传