Golang代码运行时类型复制检查器copyChecker的实现

这位 Gopher 你好呀!如果觉得我的文章还不错,欢迎一键三连支持一下!文章会定期更新,同时可以微信搜索【凑个整数1024】第一时间收到文章更新提醒⏰

今天在翻看 Golang sync 包源码时发现了一个以前从来没有仔细看过的代码实现——代码运行时类型复制检查器copyCheker,它的作用是可以在 Go 代码运行时,检测一个类型实例是否是复制的,如果是复制的则会触发paniccopyCheker目前仅被标准库中sync.Cond这个并发同步原语所使用,当一个sync.Cond变量在运行时被复制了,使用了复制的sync.Cond便会抛出panic: sync.Cond is copied,因此该检查器源码也与sync.Cond一同位于sync/cond.go

源码走读

我们直接来看一下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")
	}
}

copyChecker的源码非常简短,只有一个check方法用于检测类型复制,但它也并不容易理解。我们可以看到copyChecker底层的类型就是uintptr,Go 中专门用uintptr类型存储地址的值,因此copyChecker初始化时的零值就是0,例如sync.Cond,直接在其内部加入了copyChecker,调用sync.NewCond初始化时默认为零值:

type Cond struct {
	noCopy noCopy
	L Locker
	notify  notifyList
	checker copyChecker
}

// NewCond returns a new Cond with Locker l.
func NewCond(l Locker) *Cond {
	return &Cond{L: l}
}

sync.Cond总共有WaitSignalBroadcast三个方法,每个方法的源码中首先就会调用copyCheckercheck方法检查自己是否被复制的sync.Cond。我们接下来重点看一下check方法。

check方法中只有一个if判断,如果满足这个看起来比较复杂的条件,则会直接触发panic: sync.Cond is copied,直接报错sync.Cond被复制了。这个if判断条件其实可以看它的逆,即不触发panic的条件,这样更好理解与描述:

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

根据||可以分为三组条件,任意条件满足,都可以通过copyChecker的检查,我们来逐个解析一下:

  1. uintptr(*c) == uintptr(unsafe.Pointer(c)):不等式左边uintptr(*c)表示copychecker类型指针c所指向地址中存储的值,其实就是该copychecker本身的值;不等式右边uintptr(unsafe.Pointer(c))copychecker指针c转为 Go 语言任意指针类型unsafe.Pointer,然后转为uintptr,这样就得到了copychecker本身的存储地址,因此整个不等式的含义就是copychecker的值是否等于copychecker所在地址,即copychecker是否指向自己
  2. atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c))):对copyChecker使用了atomic包中 CAS(Compare And Swap)原子操作——如果copyChecker值为0,则将其更新为uintptr(unsafe.Pointer(c))),即copyChecker自己的地址,换句话说就是copyChecker指向了自己。如果 CAS 操作成功会返回true,否则false
  3. uintptr(*c) == uintptr(unsafe.Pointer(c)):和条件 1 完全一样,其目的是为了双重检查。由于copyChecker会被多个 goroutine 并发使用,因此如果 2 中 CAS 操作失败也有可能是别的 goroutine 抢先 CAS 操作成功,这里再进行一次检查确保copyChecker已经指向了自己

那么copyChecker为什么就能根据以上条件判断检测到复制呢?copyChecker c1在进行一次check后会指向自己,如果此时我复制了一份c2 = c1,那么c2c1的值相同,都是存储了c1所在的地址,而c2所在的地址必不可能与c1相同,因此当c2调用check,会发现c2既不指向自己,CAS 操作也无论如何不可能成功(c2不为0),条件 3 进而也一定不会成立,那么自然就会触发panic了。

我们来看一个copyChecker在检查sync.Cond被复制的例子:

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	c := sync.NewCond(new(sync.Mutex)) // 初始化一个*sync.Cond

	go func() {
		c.L.Lock()
		defer c.L.Unlock()
		c.Wait()
		fmt.Println("wait exit")
	}()

	time.Sleep(100 * time.Millisecond)
	c2 := *c // 这里c2复制了c,copyChecker会panic
	c2.Signal()
}

// OUTPUT:
// panic: sync.Cond is copied
//
// goroutine 1 [running]:
// sync.(*copyChecker).check(...)
// 		/usr/local/go/src/sync/cond.go:102
// sync.(*Cond).Signal(0x5f5e100?)
// 		/usr/local/go/src/sync/cond.go:82 +0xb8

上面这段代码中,首先初始化了一个*sync.Cond变量c,然后开启了一个新协程,在新协程内调用Wait()阻塞,此时会触发一次ccopyCheckercheck,使得copyChecker指向了自己。在主协程中,我们又声明了一个sync.Cond变量c2,这个c2直接复制了c所指向的sync.Cond结构体的值,因此sync.Cond内部的copyChecker也被复制,在c2调用Signal()时,会触发c2copyCheckercheck,根据上文描述的原理,此时就会触发panic

copyChecker vs. noCopy

如果你照着上面sync.Cond复制的例子把代码敲一遍,然后执行静态检测go vet命令,会发现c2 := *c这一行被提示了如下警告:

# copychecker
./main.go:20:8: assignment copies lock value to c2: sync.Cond contains sync.noCopy

其实如果你使用了如 Goland、VSCode(启用 lint)这种集成度高的 Go 编辑器,就会在这一行出现 warning 波浪线,提示上述警告。

我VSCode中所给出的提示,与vet工具提示的内容一致

这一警告其实归功于sync.Cond中的noCopy字段,其类型名也是noCopy,与sync.CondcopyChecker在同一文件下:

type noCopy struct{}

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

noCopy就是一个空结构体,然后有LockUnLock方法,但都是空方法,它存在的意义是什么呢?在 Go 代码中具有LockUnLock方法的类型,或者说实现了sync.Locker接口的类型,如果在使用中被拷贝了,就会触发go vet工具的警告。因此如果我们在业务开发中想自己定义一个不希望在使用时被拷贝的类型,就可以让该类型实现sync.Locker接口,或者在该类型内部定义实现sync.Locker接口的字段。

不同于本文主要讲的copyCheckernoCopy只是辅助go vet在对代码进行静态检测时抛出类型复制的警告,而copyChecker则是在运行时发现问题并报出panic

这位 Gopher 你好呀!如果觉得我的文章还不错,欢迎一键三连支持一下!文章会定期更新,同时可以微信搜索【凑个整数1024】第一时间收到文章更新提醒⏰

  • 41
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值