💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
[TOC] ## 概述 公司`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](https://golang.org/cmd/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`会复制,但是如果使用指针,您需要传递的只是一个指针。 (我的意思是,不需要花费副本)。