同步之sync.Mutex互斥锁

北战南征 提交于 2019-11-30 17:48:03

同步之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=========

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!