
引言:并发编程中的互斥需求
在Go语言中,Goroutine是轻量级的并发执行单元,它们使得编写并发程序变得简单高效。然而,当多个Goroutine同时访问或修改共享资源,或者需要确保某个特定代码段(即“临界区”)在任何给定时间只能由一个Goroutine执行时,就会出现并发控制的问题。如果不加以控制,可能会导致数据竞争、结果不确定或程序崩溃。
假设我们有三个并发执行的Goroutine,每个Goroutine都包含一个“核心操作区”(例如,涉及数据发送和打印等关键步骤)。我们的目标是,当其中一个Goroutine正在执行其核心操作区时,其他Goroutines必须被阻塞,不能进入它们自己的核心操作区,直到当前Goroutine的核心操作完成。这正是互斥锁(Mutex)所要解决的问题。
sync.Mutex:互斥锁的核心机制
Go标准库中的sync包提供了多种并发原语,其中sync.Mutex是最基础且最常用的互斥锁。它实现了“互斥访问”的概念,确保在任何时刻,只有一个Goroutine能够持有锁并执行受保护的代码段。
sync.Mutex的工作原理:
立即学习“go语言免费学习笔记(深入)”;
- Lock()方法:当一个Goroutine调用Lock()时,它会尝试获取锁。如果锁当前没有被其他Goroutine持有,该Goroutine将成功获取锁并继续执行。如果锁已被其他Goroutine持有,则调用Lock()的Goroutine将被阻塞,直到锁被释放。
- Unlock()方法:当持有锁的Goroutine完成其临界区操作后,必须调用Unlock()方法来释放锁。释放锁后,其他等待该锁的Goroutine中会有一个被唤醒,并尝试获取锁。
基本使用模式:
import "sync"
var mu sync.Mutex // 定义一个互斥锁
func protectedOperation() {
mu.Lock() // 进入临界区,获取锁
defer mu.Unlock() // 确保锁在函数退出时被释放,即使发生panic
// 这里是需要被互斥访问的临界区代码
// 只有持有锁的Goroutine才能执行到这里
}案例分析:Goroutine间的特定临界区互斥
根据我们最初的场景,有三个并发的Goroutine,它们各自包含一段需要互斥执行的核心操作。以下是实现这种特定互斥逻辑的示例代码,它展示了如何通过巧妙地组合多个sync.Mutex来达到目的。
问题描述回顾: 我们有Routine1、Routine2和Routine3。每个例程内部都有一个被星号标记的“核心操作区”(例如发送和打印操作)。要求是,当Routine1执行其核心操作区时,Routine2和Routine3必须被阻塞,不能进入它们的核心操作区,反之亦然。
解决方案剖析:
提供的解决方案使用三个独立的sync.Mutex(mutex1, mutex2, mutex3)以及一个sync.WaitGroup来协调Goroutine的启动和等待。
package main
import (
"fmt"
"sync"
"time" // 引入time包用于模拟耗时操作
"math/rand" // 引入rand包用于随机延迟
)
var (
mutex1, mutex2, mutex3 sync.Mutex // 为每个Routine定义一个主互斥锁
wg sync.WaitGroup // 用于等待所有Goroutine完成
)
func Routine1() {
mutex1.Lock() // Routine1在整个执行期间持有mutex1
defer mutex1.Unlock() // 确保mutex1在Routine1结束时被释放
defer wg.Done() // Routine1完成时通知WaitGroup
fmt.Println("Routine1: 开始执行前置操作...")
time.Sleep(time.Duration(rand.Intn(50)) * time.Millisecond) // 模拟前置操作
for i := 0; i < 5; i++ { // 模拟多次核心操作
fmt.Println("Routine1: 尝试进入核心操作区...")
// 核心互斥逻辑:Routine1的打印操作需要获取mutex2和mutex3
// 这意味着只有当Routine2和Routine3没有持有它们各自的主锁时,Routine1才能进行打印
mutex2.Lock()
mutex3.Lock()
fmt.Printf("Routine1: 核心操作 - 打印消息 %d\n", i)
time.Sleep(time.Duration(rand.Intn(20)) * time.Millisecond) // 模拟打印耗时
mutex3.Unlock()
mutex2.Unlock()
fmt.Println("Routine1: 核心操作区结束。")
}
fmt.Println("Routine1: 执行后置操作...")
time.Sleep(time.Duration(rand.Intn(50)) * time.Millisecond) // 模拟后置操作
}
func Routine2() {
mutex2.Lock() // Routine2在整个执行期间持有mutex2
defer mutex2.Unlock()
defer wg.Done()
fmt.Println("Routine2: 开始执行前置操作...")
time.Sleep(time.Duration(rand.Intn(50)) * time.Millisecond)
for i := 0; i < 5; i++ {
fmt.Println("Routine2: 尝试进入核心操作区...")
// Routine2的打印操作需要获取mutex1和mutex3
mutex1.Lock()
mutex3.Lock()
fmt.Printf("Routine2: 核心操作 - 打印消息 %d\n", i)
time.Sleep(time.Duration(rand.Intn(20)) * time.Millisecond)
mutex3.Unlock()
mutex1.Unlock()
fmt.Println("Routine2: 核心操作区结束。")
}
fmt.Println("Routine2: 执行后置操作...")
time.Sleep(time.Duration(rand.Intn(50)) * time.Millisecond)
}
func Routine3() {
mutex3.Lock() // Routine3在整个执行期间持有mutex3
defer mutex3.Unlock()
defer wg.Done()
fmt.Println("Routine3: 开始执行前置操作...")
time.Sleep(time.Duration(rand.Intn(50)) * time.Millisecond)
for i := 0; i < 5; i++ {
fmt.Println("Routine3: 尝试进入核心操作区...")
// Routine3的打印操作需要获取mutex1和mutex2
mutex1.Lock()
mutex2.Lock()
fmt.Printf("Routine3: 核心操作 - 打印消息 %d\n", i)
time.Sleep(time.Duration(rand.Intn(20)) * time.Millisecond)
mutex2.Unlock()
mutex1.Unlock()
fmt.Println("Routine3: 核心操作区结束。")
}
fmt.Println("Routine3: 执行后置操作...")
time.Sleep(time.Duration(rand.Intn(50)) * time.Millisecond)
}
func main() {
rand.Seed(time.Now().UnixNano()) // 初始化随机数种子
wg.Add(3) // 设置WaitGroup计数为3,表示有3个Goroutine需要等待
go Routine1()
go Routine2()
go Routine3() // 将Routine3也改为并发启动
wg.Wait() // 等待所有Goroutine完成
fmt.Println("所有Goroutine执行完毕。")
}代码逻辑解释:
- 主锁(mutex1, mutex2, mutex3): 每个RoutineX在函数开始时获取自己的主锁(例如Routine1获取mutex1),并在函数结束时释放。这意味着当Routine1在执行时,mutex1是被锁定的。
-
核心操作区的辅助锁:
- 在Routine1的核心操作区(fmt.Println之前),它尝试获取mutex2和mutex3。这意味着Routine1的打印操作只有在Routine2和Routine3没有持有它们各自的主锁时才能进行。
- 类似地,Routine2的核心操作需要获取mutex1和mutex3。
- Routine3的核心操作需要获取mutex1和mutex2。
- 互斥效果: 这种设计确保了在任何时刻,只有一个Goroutine能够成功获取到执行其核心操作所需的两个辅助锁。例如,如果Routine1正在执行其核心操作并成功获取了mutex2和mutex3,那么此时mutex2和mutex3是锁定的。如果Routine2或Routine3尝试进入它们的核心操作区,它们将无法获取到所需的锁(因为mutex2或mutex3已被Routine1持有),从而被阻塞。一旦Routine1释放了mutex2和mutex3,其他Goroutine就有机会获取这些锁并执行。
- sync.WaitGroup: main函数使用wg.Add(3)设置计数器,go RoutineX()启动Goroutine,每个Goroutine在defer wg.Done()确保完成后通知WaitGroup。最后wg.Wait()会阻塞main函数,直到所有Goroutine都调用了Done()。
这种特定的锁策略实现了一种复杂的协调机制,确保了核心操作的互斥性。它并非简单的“一个临界区一个锁”,而是通过交叉锁定来实现特定条件下的互斥。
更通用的临界区互斥模式
上述案例的解决方案是针对特定需求的巧妙设计。然而,在大多数并发场景中,如果目标是简单地确保某个共享资源或通用临界区在任何时刻只被一个Goroutine访问,通常会使用一个共享的sync.Mutex。
通用模式示例:
package main
import (
"fmt"
"sync"
"time"
"math/rand"
)
var commonMutex sync.Mutex // 一个所有Goroutine共享的互斥锁
func performCriticalOperation(id int) {
fmt.Printf("Goroutine %d: 准备进入临界区...\n", id)
commonMutex.Lock() // 获取共享锁
defer commonMutex.Unlock() // 确保锁在函数退出时被释放
fmt.Printf("Goroutine %d: 进入临界区,执行核心操作...\n", id)
// 模拟核心操作,例如访问共享变量、IO操作等
time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
fmt.Printf("Goroutine %d: 退出临界区。\n", id)
}
func main() {
rand.Seed(time.Now().UnixNano())
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go func(goroutineID int) {
defer wg.Done()
// 模拟一些前置操作
time.Sleep(time.Duration(rand.Intn(50)) * time.Millisecond)
performCriticalOperation(goroutineID) // 调用受互斥锁保护的操作
// 模拟一些后置操作
time.Sleep(time.Duration(rand.Intn(50)) * time.Millisecond)
}(i)
}
wg.Wait()
fmt.Println("所有Goroutine执行完毕。")
}在这个通用模式中,无论哪个Goroutine调用performCriticalOperation函数,它都必须先获取commonMutex。由于只有一个commonMutex,因此在任何给定时间,只有一个Goroutine能够成功获取它并执行临界区内的代码。这种模式简洁明了,适用于大多数需要独占访问共享资源的场景。










