Golang | sync.Mutex详解
概述
自己做的一个golang
项目需要优化,优化方向是减少gpu
内存的使用。同一个模型被重复加载多次,使用更多的gpu内存,也增加 sync.Mutex的使用。
优化的方向是:
- 减少代码量的改动- 减少gpu内存的使用,同一个模型只用加载一次 涉及的问题:
- sync.Mutex是传值还是传引用?- sync.Mutex可以拷贝么?- sync.Mutex需要申明为指针么?
sync.Mutex是传值还是传引用?
查看一个例子
//test.go
package main
import (
"fmt"
"sync"
)
func sumMutexLock1(s sync.Mutex) {
s.Lock()
fmt.Printf("sumMutexLock1, s: %p\n", &s)
defer s.Unlock()
}
func sumMutexLock2(s sync.Mutex) {
s.Lock()
fmt.Printf("sumMutexLock2, s: %p\n", &s)
defer s.Unlock()
}
func main() {
mutex := sync.Mutex{}
fmt.Printf("TestMutex21, s: %p\n", &mutex)
sumMutexLock1(mutex)
sumMutexLock2(mutex)
fmt.Println("TestMutex1")
}
运行程序后运行程序后输出
TestMutex21, s: 0xc00001e0c8
sumMutexLock1, s: 0xc00001e100
sumMutexLock2, s: 0xc00001e108
说明mutex
的值被拷贝了一份。我猜想,如果在调用函数 sumMutexLock1
之前上锁 mutex.Lock()
,则整个程序会进入死锁状态。代码加上,验证一下自己的想法,运行程序。没想到golang这么简单粗暴,程序直接panic。所以sync.Mutex是传值。
copy 结构体操作可能导致非预期的死锁
copy 结构体时,如果结构体中有锁的话,记得重新初始化一个锁对象,否则会出现非预期的死锁:
// test.go
package main
import (
"fmt"
"sync"
)
type User struct {
sync.Mutex
name string
}
func main() {
u1 := &User{name: "test"}
u1.Lock()
defer u1.Unlock()
tmp := *u1
u2 := &tmp
// u2.Mutex = sync.Mutex{} // 没有这一行就会死锁
fmt.Printf("%#p\n", u1)
fmt.Printf("%#p\n", u2)
u2.Lock()
defer u2.Unlock()
}
运行
$ go run test.go
c00000c060
c00000c080
fatal error: all goroutines are asleep - deadlock!
使用 go vet 工具检查代码中锁的使用问题
可以通过vet这个命令行来检查上面的锁 copy 的问题。比如上面的例子的检查结果如下::
$ go vet test.go
# command-line-arguments
./test.go:17:9: assignment copies lock value to tmp: command-line-arguments.User
可以看到 vet 提示 17 行那里的 copy 操作中 copy 了一个锁。
实际上 sync.Mutex
是继承nocopy
的
对于一个互斥锁,实现是一个int值 和一个uint值构成的结构体。两个值标识了锁的状态。
如果锁可以copy,那锁状态也将被copy(由于struct 是值拷贝的),当锁状态再次更新后,copy后的值将不再有效。
因此,对于实现了sync.Locker
接口的类型来说,理论上其实例是不能再次被赋值的。
golang noCopy 的实现
由于golang 中struct对象赋值是值拷贝,
golang sync 包中
-sync.Cond
-sync.Pool
-sync.WaitGroup
-sync.Mutex
-sync.RWMutex
-...
禁止拷贝,实现方式采用noCopy 的方式。
package main
import "fmt"
type noCopy struct{}
// Lock is a no-op used by -copylocks checker from `go vet`.
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
type S struct {
noCopy
data int
}
func main() {
var s S
ss := s
fmt.Println(ss)
}
golang 没有禁止对实现sync.Locker
接口的对象实例赋值进行报错,只是在使用go vet 做静态语法分析时,会提示错误。
# command-line-arguments
./nocopy.go:19: assignment copies lock value to ss: main.S
./nocopy.go:20: call of fmt.Println copies lock value: main.S
sync.Mutex需要申明为指针么?
如果使用 指针*sync.Mutex
,拷贝指针是不是相当于持有了同一把锁了呢。虽然指针的基本原理都知道,指针存储的是指向对象的地址,拷贝指针也就是拷贝指向对象的地址,但我还是写代码验证一下:
package main
import (
"fmt"
"sync"
"time"
)
type Container struct {
mutex *sync.Mutex
wg sync.WaitGroup
count int
}
func NewContainer() *Container {
return &Container{
mutex: new(sync.Mutex),
wg: sync.WaitGroup{},
count: 0,
}
}
func (c *Container)start() {
c.wg.Add(1000)
for i := 0; i < 500; i++ {
go c.sumMutexLock1(c.mutex) //把锁以指针的形式传进去
go c.sumMutexLock2(c.mutex) //把锁以指针的形式传进去
}
c.wg.Wait()
fmt.Printf("start, counts: %d\n\n", c.count)
}
func (c *Container)sumMutexLock1(s *sync.Mutex) {
defer c.wg.Done()
s.Lock() //使用拷贝进来的指针锁加锁
c.count++
fmt.Printf("sumMutexLock1, count: %d\n", c.count)
s.Unlock() //使用拷贝进来的指针锁解锁
time.Sleep(time.Second*2)
}
func (c * Container)sumMutexLock2(s *sync.Mutex) {
defer c.wg.Done()
s.Lock() //使用拷贝进来的指针锁加锁
c.count++
c.mutex.Unlock()
fmt.Printf("sumMutexLock2, counts: %d\n", c.count)
time.Sleep(time.Second*1)
}
func main() {
c := NewContainer()
c.start()
}
看到最后结果输出的是:
start, counts: 1000
说明拷贝锁的指针,相当于持有了同一把锁。
总结
sync.Mutex
是传值,如果copy结构体
可能能会导致死锁,sync.Mutex
是noCopy
的。;- 我认为可以将* sync.Mutex
视为简单指针。如果你想要使用它,您应该声明并初始化它,但是如果使用sync.Mutex
,它已经被初始化。- 我认为需要声明为指针*sync.Mutex
,它们总是传递要使用的可变指针,因为传递struct
会复制,但是如果使用指针,您需要传递的只是一个指针。 (我的意思是,不需要花费副本)。
--完--
- 原文作者: 留白
- 原文链接: https://zfunnily.github.io/2020/12/syncmutex/
- 更新时间:2024-04-16 01:01:05
- 本文声明:转载请标记原文作者及链接