这位 Gopher 你好呀!如果觉得我的文章还不错,欢迎一键三连支持一下!文章会定期更新,同时可以微信搜索【凑个整数1024】第一时间收到文章更新提醒⏰
今天在翻看 Golang sync 包源码时发现了一个以前从来没有仔细看过的代码实现——代码运行时类型复制检查器copyCheker
,它的作用是可以在 Go 代码运行时,检测一个类型实例是否是复制的,如果是复制的则会触发panic
。copyCheker
目前仅被标准库中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
总共有Wait
、Signal
和Broadcast
三个方法,每个方法的源码中首先就会调用copyChecker
的check
方法检查自己是否被复制的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
的检查,我们来逐个解析一下:
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
是否指向自己;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
;uintptr(*c) == uintptr(unsafe.Pointer(c))
:和条件 1 完全一样,其目的是为了双重检查。由于copyChecker
会被多个 goroutine 并发使用,因此如果 2 中 CAS 操作失败也有可能是别的 goroutine 抢先 CAS 操作成功,这里再进行一次检查确保copyChecker
已经指向了自己。
那么copyChecker
为什么就能根据以上条件判断检测到复制呢?copyChecker
c1
在进行一次check
后会指向自己,如果此时我复制了一份c2 = c1
,那么c2
和c1
的值相同,都是存储了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()
阻塞,此时会触发一次c
中copyChecker
的check
,使得copyChecker
指向了自己。在主协程中,我们又声明了一个sync.Cond
变量c2
,这个c2
直接复制了c
所指向的sync.Cond
结构体的值,因此sync.Cond
内部的copyChecker
也被复制,在c2
调用Signal()
时,会触发c2
中copyChecker
的check
,根据上文描述的原理,此时就会触发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 波浪线,提示上述警告。
这一警告其实归功于sync.Cond
中的noCopy
字段,其类型名也是noCopy
,与sync.Cond
和copyChecker
在同一文件下:
type noCopy struct{}
// Lock is a no-op used by -copylocks checker from `go vet`.
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
noCopy
就是一个空结构体,然后有Lock
和UnLock
方法,但都是空方法,它存在的意义是什么呢?在 Go 代码中具有Lock
和UnLock
方法的类型,或者说实现了sync.Locker
接口的类型,如果在使用中被拷贝了,就会触发go vet
工具的警告。因此如果我们在业务开发中想自己定义一个不希望在使用时被拷贝的类型,就可以让该类型实现sync.Locker
接口,或者在该类型内部定义实现sync.Locker
接口的字段。
不同于本文主要讲的copyChecker
,noCopy
只是辅助go vet
在对代码进行静态检测时抛出类型复制的警告,而copyChecker
则是在运行时发现问题并报出panic
。
这位 Gopher 你好呀!如果觉得我的文章还不错,欢迎一键三连支持一下!文章会定期更新,同时可以微信搜索【凑个整数1024】第一时间收到文章更新提醒⏰