同步之sync.Mutex互斥锁
sync包中定义了Locker结构来代表锁。
type Locker interface {
Lock()
Unlock()
}
并且定义了两个结构来实现Locker接口:Mutex 和 RWMutex。
我们可以用一个容量只有1的channel来保证最多只有一个goroutine在同一时刻访问一个共享变量。一个只能为1和0的信号量叫做二元信号量(binary semaphore)。使用二元信号量实现互斥锁,示例如下,
var (
// 一个二元信号量
// 缓存为 1 的channel
sema = make(chan struct{}, 1) // a binary semaphore guarding balance
balance int
)
func Deposit(amount int) {
// 存钱的时候需要获取一个信号量,以此来保护变量 balance
sema <- struct{}{} // acquire token
balance = balance + amount
<-sema // release token
}
func Balance() int {
sema <- struct{}{} // acquire token
b := balance
<-sema // release token
return b
}
使用互斥锁sync.Mutex实现示例如下,
import (
"sync"
)
var (
mu sync.Mutex // guards balance
balance int
)
func Deposit(amount int) {
mu.Lock()
balance = balance + amount
mu.Unlock()
}
func Balance() int {
mu.Lock()
b := balance
mu.Unlock()
return b
}
Go语言里的sync.Mutex和Java里的ReentrantLock都实现互斥的语义,但有一个很大的区别就是锁的可重入性。
ReentrantLock 意味着什么呢?简单来说,它有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。这模仿了 synchronized 的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续) synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个 synchronized 块时,才释放锁。
ReentrantLock的可重入性用代码来表示如下,
class Parent {
protected Lock lock = new ReentrantLock();
public void test() {
lock.lock();
try {
System.out.println("Parent");
} finally {
lock.unlock();
}
}
}
class Sub extends Parent {
@Override
public void test() {
lock.lock();
try {
super.test();
System.out.println("Sub");
} finally {
lock.unlock();
}
}
}
public class AppTest {
public static void main(String[] args) {
Sub s = new Sub();
s.test();
}
}
要注意到需要做两次释放锁的操作。
而Go语言的sync.Mutex互斥锁没有可重入的特性,看下面这段代码,
import (
"sync"
)
var (
mu sync.Mutex // guards balance
balance int
)
func Deposit(amount int) {
mu.Lock()
balance = balance + amount
mu.Unlock()
}
func Balance() int {
mu.Lock()
b := balance
mu.Unlock()
return b
}
// NOTE: incorrect!
func Withdraw(amount int) bool {
mu.Lock()
defer mu.Unlock()
Deposit(-amount)
if Balance() < 0 {
Deposit(amount)
return false // insufficient funds
}
return true
}
上面这个例子中,Deposit会调用mu.Lock()第二次去获取互斥锁,但因为mutex已经锁上了,而无法被重入——也就是说没法对一个已经锁上的mutex来再次上锁--这会导致程序死锁,没法继续执行下去,Withdraw会永远阻塞下去。
一个通用的解决方案是将一个函数分离为多个函数,比如我们把Deposit分离成两个:一个不导出的函数deposit,这个函数假设锁总是会被保持并去做实际的操作,另一个是导出的函数Deposit,这个函数会调用deposit,但在调用前会先去获取锁。同理我们可以将Withdraw也表示成这种形式:
func Withdraw(amount int) bool {
mu.Lock()
defer mu.Unlock()
deposit(-amount)
if balance < 0 {
deposit(amount)
return false // insufficient funds
}
return true
}
func Deposit(amount int) {
mu.Lock()
defer mu.Unlock()
deposit(amount)
}
// This function requires that the lock be held.
func deposit(amount int) { balance += amount }
=========END=========
来源:oschina
链接:https://my.oschina.net/u/1469576/blog/717716