一、小案例
package main
import (
"fmt"
"sync"
)
var (
count int // 待加锁的字段
countLock sync.Mutex // 对应的互斥锁
)
// 在临界区前后加锁
func getCount() int {
countLock.Lock()
defer countLock.Unlock()
return count
}
// 在临界区前后加锁
func setCount(c int) {
countLock.Lock()
count = c
countLock.Unlock()
}
func main() {
setCount(100)
fmt.Println(getCount())
}
我们可以总结如何使用一个互斥锁:
-
对待加锁的变量(或实例字段)添加一个对应的
sync.Mutex -
只能通过封装好的函数访问该变量/字段。
-
每一个封装好的函数都在一开始就获取互斥锁,并在最后释放锁。
-
每次,一个
goroutine调用这些函数时,都要调用countLock.Lock()来获取一个互斥锁。如果其它的goroutine已经获得了这个锁的话,这个操作会被阻塞直到其它goroutine调用了countLock.Unlock()使该锁变回可用状态。
二、与Java的异同
Java 有「可重入锁」的概念,指的是同一个线程在运行过程中可以重复获取同一个锁(此时其它线程仍然不可获取该锁)。但 Go 没有可重入锁的概念,因此以下例子是错误的:
import "sync"
var (
mu sync.Mutex
balance int
)
func Deposit(amount int) {
mu.Lock()
balance = balance + amount
mu.Unlock()
}
func Balance() int {
mu.Lock()
b := balance
mu.Unlock()
return b
}
// 错误!
func Withdraw(amount int) bool {
// 此时已经对 mu 加锁一次
mu.Lock()
defer mu.Unlock()
Deposit(-amount) // Deposit() 无法再次加锁
if Balance() < 0 {
Deposit(amount)
return false
}
return true
}
withdraw() 已经将mu加锁一次,随后调用 Deposit() 将无法再次加锁,只会无限阻塞下去。