Go为什么不支持可重入锁呢?

先来看看什么是可重入锁,以下是维基百科的定义。

计算机科学中,可重入互斥锁(英语:reentrant mutex)是互斥锁的一种,同一线程对其多次加锁不会产生死锁。可重入互斥锁也称递归互斥锁(英语:recursive mutex)或递归锁(英语:recursive lock)。

如果对已经上锁的普通互斥锁进行“加锁”操作,其结果要么失败,要么会阻塞至解锁。而如果换作可重入互斥锁,当且仅当尝试加锁的线程就是持有该锁的线程时,类似的加锁操作就会成功。可重入互斥锁一般都会记录被加锁的次数,只有执行相同次数的解锁操作才会真正解锁。

递归互斥锁解决了普通互斥锁不可重入的问题:如果函数先持有锁,然后执行回调,但回调的内容是调用它自己,就会产生死锁[1]

Go里面是不支持可重入互斥锁的。看代码:

var mu sync.Mutex
func main() {
	mu.Lock()
	mu.Lock()
}

运行后会报

fatal error: all goroutines are asleep - deadlock!

Go 设计原则

在工程中使用互斥的根本原因是:为了保护不变量,也可以用于保护内、外部的不变量。

基于此,Go 在互斥锁设计上会遵守这几个原则。如下:

  • 在调用 mutex.Lock 方法时,要保证这些变量的不变性保持,不会在后续的过程中被破坏。
  • 在调用 mu.Unlock 方法时,要保证:
    • 程序不再需要依赖那些不变量。
    • 如果程序在互斥锁加锁期间破坏了它们,则需要确保已经恢复了它们。

不支持的原因

讲了 Go 自己的设计原则后,那为什么不支持可重入呢?

其实 Russ Cox 于 2010 年在《Experimenting with GO》就给出了答复,认为递归(又称:重入)互斥是个坏主意,这个设计并不好。

总结

Go 互斥锁没有支持可重入锁的设计,这样就不用关心递归层级等复杂的流程了。加锁解锁中间做了什么一目了然。

关于Go切片截取的一个注意事项

go在使用切片截断时,底层数组没有改变,依然持有指向堆内存的指针,导致内存无法释放。

func split(arr []int) []int {
	newArr := arr[0:5]
	return newArr
}

以上切片newArr截取了切片arr中的部分数据并返回。如果切片arr在之后不再使用,即使切片newArr只使用了切片arr中的前5个数据而已,arr的整个底层数组都不会被GC回收。如果切片arr中含有大量的数据,一直不释放,那这会造成较大的浪费内存。

类似这种可以在截断前不需要的手动置空。

arr[5:] = nil
newArr := arr[0:5]

另外对于项目中Go代码情况可以pprof程序性能分析工具去查看。