A Mutex must not be copied after first use. 是什么(nocopy)

本文探讨了Go语言中为何需要防止某些结构体被复制,特别是涉及同步和安全的类型如`sync.Cond`和`sync.Mutex`。通过运行时检查和`govet`静态检查两种方法,保证了这些类型在首次使用后不能被安全地复制,以避免潜在的并发安全问题。文章还提供了示例代码解释了这两种策略的工作原理。
摘要由CSDN通过智能技术生成

首先,为啥需要nocopy?

对 mutex 的copy是不安全的,即使一个mutex释放了这把锁,那个被copy的mutex还是没有释放,这是不安全的。

那么如何做到?今天去搜这个东西,搜到一篇 blog,感觉讲的可以,所以翻译一下。

原文:https://bronzesword.medium.com/what-does-nocopy-after-first-use-mean-in-golang-and-how-12396c31de47

作者:jing

翻译:Google翻译

校对:我

当我们阅读 golang 源码或学习使用一些内置 struct 时,经常会被告知“首次使用后不能复制”,例如 sync.Cond、sync.Map、sync.Mutex(几乎 sync 包中所有类型都有 ) 和strings.Builder。出于安全原因,大多数情况下这是必需的。

例如,您有一个带有指针字段的 struct 并且您不希望它被复制,因为浅拷贝会使这两个对象(struct 的实例)持有相同指针,这是不安全的。那么 golang 做了什么来确保这一点呢?据我所知,没有一个完美的答案(请参阅此处的讨论)。但在这个blog中,我将回顾以下两种解决方案,并希望它能提供更好的理解方式。

运行时检查

这是通过封装一个指向自身的指针并在任何进一步操作之前检查来完成的。一个典型的例子是 strings.Builder :

type Builder struct {
    addr *Builder
    buf []byte
}
func (b *Builder) copyCheck() {
    if b.addr == nil {
       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"))   // will panic here

如您所见,当我们声明builder a并写入时,a.addr将被分配变量 a 的地址。我们把a赋值给b之后,a.addr会被浅拷贝到b.addr,但是b新分配的地址肯定和a.addr不一样,所以panic就发生了。该解决方案利用了指针被浅拷贝的事实且易于理解。

另一个简单的例子是 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()
    ...
}

乍一看 check() 函数有点难以理解,所以让我们定义一个类似的结构并尝试找出原因:

type cond struct {
    checker copyChecker
}
type copyChecker uintptr
func (c *copyChecker) check() {
    fmt.Printf("Before: c: %v, *c: %v, uintptr(*c): %v, uintptr(unsafe.Pointer(c)): %v\n", c, *c, uintptr(*c), uintptr(unsafe.Pointer(c)))
    atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c)))
    fmt.Printf("After: c: %v, *c: %v, uintptr(*c): %v, uintptr(unsafe.Pointer(c)): %v\n", c, *c, uintptr(*c), uintptr(unsafe.Pointer(c)))
}
// test case
var a cond
a.checker.check()
b := a
b.checker.check()
// results
Before: c: 0x414020, *c: 0, uintptr(*c): 0, uintptr(unsafe.Pointer(c)): 4276256
After: c: 0x414020, *c: 4276256, uintptr(*c): 4276256, uintptr(unsafe.Pointer(c)): 4276256
Before: c: 0x414044, *c: 4276256, uintptr(*c): 4276256, uintptr(unsafe.Pointer(c)): 4276292
After: c: 0x414044, *c: 4276256, uintptr(*c): 4276256, uintptr(unsafe.Pointer(c)): 4276292

很明显,当我们声明 a 时,它的 checker 字段是 0 并且 checker 字段的地址是 0x414020 或十进制的 4276256 。在 CompareAndSwapUintptr() 之后,它的 checker 字段被分配了自己的地址,比如 4276256 。当我们将 a 赋值给 b 时,a 的 checker 域被复制到 b 的域,但是 b 的 checker 域的地址实际上是 0x414044 或十进制的 4276292。所以它最终满足了所有三个条件(实际上是两个)并检测到复制。这仍然是一种“自指针”方法,我想你明白了。 总而言之,运行时检查通常使用自指针,直到运行时才会检查。

总而言之,运行时检查通常使用自指针,直到运行时才会检查。

go vet copylocks 检查

-copylocks 实际上是一个 go vet 标志,用于检查 locker 类型是否被复制。locker 类型是具有 Lock() 和 Unlock() 方法的类型。(有关更多详细信息,请参见此处)。如前所述,几乎所有同步包中的类型都不能被复制,实际上它只是通过封装一个 noCopy 结构来保证的:

// src/sync/cond.go
type noCopy struct{}
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
// sync.Pool
type Pool struct {
   noCopy noCopy  
   ...  
}
// sync.WaitGroup 
type WaitGroup struct {
   noCopy noCopy  
   ...
}

Go vet 将检查一个 locker 的每个语句和操作,这样可以在运行前找到copy操作。因此,如果您希望不复制类型,您只需在包中定义一个 noCopy 结构并将其封装为额外字段,如下所示:

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

go vet 就可以保证没有 copy 了

结论 尽管目前我们对这个问题还没有一个完美的答案,但这两种方法在类似的情况下仍然具有指导意义。自我指针或去使用go vet?下次需要的时候可以试试看 :)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值