返回

Go 语言并发原语:深入剖析 Mutex、RWMutex、Cond、WaitGroup 和 Once 的使用和实现

后端

Go 语言并发基础

在上一篇文章中,我们讨论了基于 CSP(通信顺序进程)模型的 Go 语言并发,重点介绍了 goroutine、channel 和 select 的使用。除了 CSP,Go 语言还提供了功能强大的 sync 包和 atomic 包,其中包含了一组用于并发控制的基本原语。

这些原语使我们能够同步对共享资源的访问,协调 goroutine 之间的通信,并处理并发编程中常见的挑战。在本文中,我们将深入探讨以下原语的使用和底层实现:

  • Mutex
  • RWMutex
  • Cond
  • WaitGroup
  • Once

Mutex

Mutex(互斥锁)是一种用于保护共享资源免受并发访问的锁机制。它确保一次只有一个 goroutine 可以访问资源,从而防止数据竞争和程序崩溃。

使用 Mutex

使用 Mutex 非常简单。首先,您需要创建一个 Mutex 类型的值:

var myMutex sync.Mutex

然后,您可以在需要保护的代码块周围使用 Mutex 的 Lock() 和 Unlock() 方法:

func myFunction() {
    myMutex.Lock()
    // 对共享资源进行操作
    myMutex.Unlock()
}

底层实现

Mutex 的底层实现基于自旋锁。自旋锁是一种轻量级的同步机制,它通过在获取锁之前不断检查锁的状态来减少竞争和上下文切换的开销。

当一个 goroutine 尝试获取锁时,它将不断检查锁的标志位。如果锁可用,它将原子地将标志位设置为已锁定,并立即获得锁。否则,它将进入自旋状态,不断检查标志位,直到锁可用。

RWMutex

RWMutex(读写互斥锁)是一种特殊的 Mutex,它允许多个 goroutine 同时读取共享资源,但一次只能有一个 goroutine 写入资源。这使得 RWMutex 非常适合需要经常读取但偶尔写入的共享数据结构。

使用 RWMutex

与 Mutex 类似,首先创建一个 RWMutex 类型的值:

var myRWMutex sync.RWMutex

然后,您可以使用 RWMutex 的 RLock() 和 RUnlock() 方法来获取读锁,以及 Lock() 和 Unlock() 方法来获取写锁:

func myFunction() {
    myRWMutex.RLock()
    // 读取共享资源
    myRWMutex.RUnlock()
}

func myOtherFunction() {
    myRWMutex.Lock()
    // 写入共享资源
    myRWMutex.Unlock()
}

底层实现

RWMutex 的底层实现使用了一个计数器和一个锁标志位。计数器跟踪当前有多少个 goroutine 持有读锁。如果计数器为零,则锁标志位表示锁是否被写 goroutine 持有。

当一个 goroutine 尝试获取读锁时,它将增加计数器。如果计数器非零且锁标志位未设置,则 goroutine 将立即获得读锁。否则,它将进入自旋状态,等待其他 goroutine 释放锁。

当一个 goroutine 尝试获取写锁时,它将检查计数器是否为零。如果计数器非零,则 goroutine 将被阻塞,直到所有读 goroutine 释放锁。然后,它将获取写锁并设置锁标志位。

Cond

Cond(条件变量)是一种用于协调 goroutine 之间通信的同步机制。它允许一个 goroutine 等待另一个 goroutine 满足某个条件。

使用 Cond

要使用 Cond,您需要创建一个 Cond 类型的值:

var myCond sync.Cond

然后,您可以使用 Cond 的 Wait() 和 Signal() 方法:

func myFunction() {
    myCond.L.Lock()
    for !condition {
        myCond.Wait()
    }
    myCond.L.Unlock()
}

func myOtherFunction() {
    myCond.L.Lock()
    condition = true
    myCond.Signal()
    myCond.L.Unlock()
}

在上面的示例中,myFunction() 将被阻塞,直到 myOtherFunction() 设置 condition 为 true 并调用 Signal() 方法。

底层实现

Cond 的底层实现使用了一个等待队列和一个通知标志位。等待队列存储着正在等待条件的 goroutine。通知标志位表示条件是否已满足。

当一个 goroutine 调用 Wait() 方法时,它将自己添加到等待队列并释放锁。然后,它将进入睡眠状态,直到被 Signal() 方法唤醒。

当一个 goroutine 调用 Signal() 方法时,它将设置通知标志位并唤醒等待队列中的一个 goroutine。唤醒的 goroutine 将重新获取锁并继续执行。

WaitGroup

WaitGroup 是一种用于协调多个 goroutine 完成任务的同步机制。它允许一个 goroutine 等待其他 goroutine 完成他们的任务。

使用 WaitGroup

要使用 WaitGroup,您需要创建一个 WaitGroup 类型的值:

var myWaitGroup sync.WaitGroup

然后,您可以使用 WaitGroup 的 Add() 和 Done() 方法:

func myFunction() {
    myWaitGroup.Add(1)
    // 执行任务
    myWaitGroup.Done()
}

func myOtherFunction() {
    myWaitGroup.Wait()
}

在上面的示例中,myFunction() 将增加 WaitGroup 的计数器,表示它已开始执行任务。myOtherFunction() 将等待 WaitGroup 的计数器变为零,表示所有任务已完成。

底层实现

WaitGroup 的底层实现使用了一个计数器和一个锁。计数器跟踪当前正在执行的任务数。锁用于防止多个 goroutine 同时访问计数器。

当一个 goroutine 调用 Add() 方法时,它将增加计数器。当一个 goroutine 调用 Done() 方法时,它将减少计数器。

当一个 goroutine 调用 Wait() 方法时,它将不断检查计数器是否为零。如果计数器非零,它将进入自旋状态,等待其他 goroutine 调用 Done() 方法。

Once

Once 是一种用于确保一段代码只执行一次的同步机制。这对于初始化单例或执行其他需要保证只执行一次的操作非常有用。

使用 Once

要使用 Once,您需要创建一个 Once 类型的值:

var myOnce sync.Once

然后,您可以使用 Once 的 Do() 方法:

func myFunction() {
    myOnce.Do(func() {
        // 只执行一次的代码
    })
}

在上面的示例中,myFunction() 将仅执行一次 Do() 方法中提供的函数。

底层实现

Once 的底层实现使用了一个原子标志位。该标志位表示代码是否已执行。

当一个 goroutine 调用 Do() 方法时,它将检查标志位。如果标志位未设置,它将设置标志位并执行提供的函数。否则,它将跳过函数。