个人博客导航页(点击右侧链接即可打开个人博客):大牛带你入门技术栈
一、互斥锁
互斥锁是传统的并发程序对共享资源进行访问控制的主要手段。它由标准库代码包sync中的Mutex结构体类型代表。sync.Mutex类型(确切地说,是*sync.Mutex类型)只有两个公开方法——Lock和Unlock。顾名思义,前者被用于锁定当前的互斥量,而后者则被用来对当前的互斥量进行解锁。 类型sync.Mutex的零值表示了未被锁定的互斥量。也就是说,它是一个开箱即用的工具。我们只需对它进行简单声明就可以正常使用了,就像这样: 代码如下:
var mutex sync.Mutex
mutex.Lock()
在我们使用其他编程语言(比如C或Java)的锁类工具的时候,可能会犯的一个低级错误就是忘记及时解开已被锁住的锁,从而导致诸如流程执行异常、线程执行停滞甚至程序死锁等等一系列问题的发生。然而,在Go语言中,这个低级错误的发生几率极低。其主要原因是有defer语句的存在。 我们一般会在锁定互斥锁之后紧接着就用defer语句来保证该互斥锁的及时解锁。请看下面这个函数: 代码如下:
var mutex sync.Mutex
func write() {
mutex.Lock()
defer mutex.Unlock()
// 省略若干条语句
}
函数write中的这条defer语句保证了在该函数被执行结束之前互斥锁mutex一定会被解锁。这省去了我们在所有return语句之前以及异常发生之时重复的附加解锁操作的工作。在函数的内部执行流程相对复杂的情况下,这个工作量是不容忽视的,并且极易出现遗漏和导致错误。所以,这里的defer语句总是必要的。在Go语言中,这是很重要的一个惯用法。我们应该养成这种良好的习惯。 对于同一个互斥锁的锁定操作和解锁操作总是应该成对的出现。如果我们锁定了一个已被锁定的互斥锁,那么进行重复锁定操作的Goroutine将会被阻塞,直到该互斥锁回到解锁状态。请看下面的示例: 代码如下:
func repeatedlyLock() {
var mutex sync.Mutex
fmt.Println("Lock the lock. (G0)")
mutex.Lock()
fmt.Println("The lock is locked. (G0)")
for i := 1; i <= 3; i++ {
go func(i int) {
fmt.Printf("Lock the lock. (G%d)", i)
mutex.Lock()
fmt.Printf("The lock is locked. (G%d)", i)
}(i)
}
time.Sleep(time.Second)
fmt.Println("Unlock the lock. (G0)")
mutex.Unlock()
fmt.Println("The lock is unlocked. (G0)")
time.Sleep(time.Second)
}
我们把执行repeatedlyLock函数的Goroutine称为G0。而在repeatedlyLock函数中,我们又启用了3个Goroutine,并分别把它们命名为G1、G2和G3。可以看到,我们在启用这3个Goroutine之前就已经对互斥锁mutex进行了锁定,并且在这3个Goroutine将要执行的go函数的开始处也加入了对mutex的锁定操作。这样做的意义是模拟并发地对同一个互斥锁进行锁定的情形。当for语句被执行完毕之后,我们先让G0小睡1秒钟,以使运行时系统有充足的时间开始运行G1、G2和G3。在这之后,解锁mutex。为了能够让读者更加清晰地了解到repeatedlyLock函数被执行的情况,我们在这些锁定和解锁操作的前后加入了若干条打印语句,并在打印内容中添加了我们为这几个Goroutine起的名字。也由于这个原因,我们在repeatedlyLock函数的最后再次编写了一条“睡眠”语句,以此为可能出现的其他打印内容再等待一小会儿。 经过短暂的执行,标准输出上会出现如下内容: 代码如下:
Lock the lock. (G0)
The lock is locked. (G0)
Lock the lock. (G1)
Lock the lock. (G2)
Lock the lock. (G3)
Unlock the lock. (G0)
The lock is unlocked. (G0)
The lock is locked. (G1)
从这八行打印内容中,我们可以清楚的看出上述四个Goroutine的执行情况。首先,在repeatedlyLock函数被执行伊始,对互斥锁的第一次锁定操作便被进行并顺利地完成。这由第一行和第二行打印内容可以看出。而后,在repeatedlyLock函数中被启用的那三个Goroutine在G0的第一次“睡眠”期间开始被运行。当相应的go函数中的对互斥锁的锁定操作被进行的时候,它们都被阻塞住了。原因是该互斥锁已处于锁定状态了。这就是我们在这里只看到了三个连续的Lock the lock. (G*)而没有立即看到The lock is locked. (G*)的原因。随后,G0“睡醒”并解锁互斥锁。这使得正在被阻塞的G1、G2和G3都会有机会重新锁定该互斥锁。但是,只有一个Goroutine会成功。成功完成锁定操作的某一个Goroutine会继续执行在该操作之后的语句。而其他Goroutine将继续被阻塞,直到有新的机会到来。这也就是上述打印内容中的最后三行所表达的含义。显然,G1抢到了这次机会并成功锁定了那个互斥锁。 实际上,我们之所以能够通过使用互斥锁对共享资源的唯一性访问进行控制正是因为它的这一特性。这有效的对竞态条件进行了消除。 互斥锁的锁定操作的逆操作并不会引起任何Goroutine的阻塞。但是,它的进行有可能引发运行时恐慌。更确切的讲,当我们对一个已处于解锁状态的互斥锁进行解锁操作的时候,就会已发一个运行时恐慌。这种情况很可能会出现在相对复杂的流程之中——我