[Go] 并发操作 · 快速了解互斥锁 sync.Mutex

119 阅读1分钟

一、小案例

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())
}

我们可以总结如何使用一个互斥锁:

  1. 对待加锁的变量(或实例字段)添加一个对应的sync.Mutex

  2. 只能通过封装好的函数访问该变量/字段。

  3. 每一个封装好的函数都在一开始就获取互斥锁,并在最后释放锁。

  4. 每次,一个 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() 将无法再次加锁,只会无限阻塞下去。