
在Go语言中,Goroutine是轻量级的并发执行单元,它们使得编写并发程序变得简单高效。然而,当多个Goroutine同时访问或修改共享资源,或者需要确保某个特定代码段(即“临界区”)在任何给定时间只能由一个Goroutine执行时,就会出现并发控制的问题。如果不加以控制,可能会导致数据竞争、结果不确定或程序崩溃。
假设我们有三个并发执行的Goroutine,每个Goroutine都包含一个“核心操作区”(例如,涉及数据发送和打印等关键步骤)。我们的目标是,当其中一个Goroutine正在执行其核心操作区时,其他Goroutines必须被阻塞,不能进入它们自己的核心操作区,直到当前Goroutine的核心操作完成。这正是互斥锁(Mutex)所要解决的问题。
Go标准库中的sync包提供了多种并发原语,其中sync.Mutex是最基础且最常用的互斥锁。它实现了“互斥访问”的概念,确保在任何时刻,只有一个Goroutine能够持有锁并执行受保护的代码段。
sync.Mutex的工作原理:
立即学习“go语言免费学习笔记(深入)”;
基本使用模式:
import "sync"
var mu sync.Mutex // 定义一个互斥锁
func protectedOperation() {
mu.Lock() // 进入临界区,获取锁
defer mu.Unlock() // 确保锁在函数退出时被释放,即使发生panic
// 这里是需要被互斥访问的临界区代码
// 只有持有锁的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执行完毕。")
}代码逻辑解释:
这种特定的锁策略实现了一种复杂的协调机制,确保了核心操作的互斥性。它并非简单的“一个临界区一个锁”,而是通过交叉锁定来实现特定条件下的互斥。
上述案例的解决方案是针对特定需求的巧妙设计。然而,在大多数并发场景中,如果目标是简单地确保某个共享资源或通用临界区在任何时刻只被一个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能够成功获取它并执行临界区内的代码。这种模式简洁明了,适用于大多数需要独占访问共享资源的场景。
以上就是Go语言中并发Goroutine临界区互斥的实现的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号