概述

自己做的一个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.MutexnoCopy的。;- 我认为可以将* sync.Mutex视为简单指针。如果你想要使用它,您应该声明并初始化它,但是如果使用sync.Mutex,它已经被初始化。- 我认为需要声明为指针 *sync.Mutex,它们总是传递要使用的可变指针,因为传递struct会复制,但是如果使用指针,您需要传递的只是一个指针。 (我的意思是,不需要花费副本)。

--完--