Go 并发编程:Goroutine、闭包、互斥锁和 WaitGroup
Go 语言以其强大的并发能力著称,提供了 Goroutine、互斥锁和 WaitGroup 等工具,让开发者可以轻松处理并发操作。
1. Goroutine 并发执行与非确定性
在 Go 中,Goroutine 是实现并发的基本单元。使用 go 关键字可以轻松创建 Goroutine,实现多任务的同时执行。每个 Goroutine 是一个独立的执行单元,它的执行顺序不确定,因为 Go 运行时会自动调度各个 Goroutine 的执行。
代码示例:
package main
import (
"fmt"
"time"
)
func printNumber(num int) {
fmt.Println("Number:", num)
}
func main() {
for i := 0; i < 5; i++ {
go printNumber(i) // 启动多个 Goroutine
}
time.Sleep(time.Second) // 主 Goroutine 暂停,确保子 Goroutine 有机会执行
}
输出示例:
Number: 3
Number: 1
Number: 0
Number: 4
Number: 2
说明:每次运行的输出顺序可能不同,因为 Goroutine 是并发执行的。这种不确定性是 Go 并发的一个重要特点,在实际编程中需要考虑这种行为。
2. 闭包中的参数捕获问题
在 Go 的并发编程中,使用闭包创建 Goroutine 时要小心循环变量的捕获问题。直接在 Goroutine 的匿名函数中使用循环变量(如 i)会导致每个 Goroutine 都引用同一个变量的最终值,而不是它们启动时的值。这是因为闭包会捕获变量而不是它们的瞬时值。
解决方法:将循环变量作为参数传递给匿名函数,确保每个 Goroutine 能独立使用正确的值。
代码示例:
package main
import (
"fmt"
"time"
)
func printNumber(num int) {
fmt.Println("Number:", num)
}
func main() {
for i := 0; i < 5; i++ {
go func(j int) {
printNumber(j)
}(i) // 将循环变量 `i` 作为参数传递给匿名函数
}
time.Sleep(time.Second) // 主 Goroutine 暂停,确保子 Goroutine 有机会执行
}
输出示例:
Number: 0
Number: 1
Number: 2
Number: 3
Number: 4
说明:通过将
i作为参数j传递给匿名函数,可以确保每个 Goroutine 打印的num是唯一的,避免了闭包捕获循环变量导致的问题。
3. 使用 sync.Mutex 实现并发安全
当多个 Goroutine 同时访问和修改共享数据时,可能会发生数据竞争(Race Condition),导致计算结果不正确。Go 提供了 sync.Mutex 互斥锁,确保同一时间只有一个 Goroutine 可以访问共享资源。
使用方法:
lock.Lock():对资源加锁,防止其他 Goroutine 干扰。lock.Unlock():释放锁,允许其他 Goroutine 访问资源。
代码示例:
package main
import (
"fmt"
"sync"
)
var (
x int
lock sync.Mutex
)
func increment(wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 1000; i++ {
lock.Lock() // 加锁
x += 1 // 对共享资源进行操作
lock.Unlock() // 解锁
}
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go increment(&wg)
}
wg.Wait()
fmt.Println("Final value of x:", x)
}
输出示例:
Final value of x: 5000
说明:使用互斥锁后,保证了并发安全,每次执行的最终输出结果都是正确的
5000。如果不加锁,多个 Goroutine 会同时访问x,导致结果小于预期值。
4. 使用 sync.WaitGroup 实现 Goroutine 同步
在 Go 中,主 Goroutine 不会等待其他 Goroutine 执行完成,而是继续执行主程序,可能导致子 Goroutine 尚未执行完,主程序就已退出。sync.WaitGroup 可以解决这个问题,通过计数机制来确保所有 Goroutine 完成后再继续执行。
使用方法:
wg.Add(n):设置需要等待的 Goroutine 数量。wg.Done():在 Goroutine 完成时调用,减少计数。wg.Wait():阻塞主 Goroutine,直到计数归零。
代码示例:
package main
import (
"fmt"
"sync"
)
func printNumber(wg *sync.WaitGroup, num int) {
defer wg.Done() // 在 Goroutine 完成时调用 Done 减少计数
fmt.Println("Number:", num)
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1) // 每启动一个 Goroutine 增加计数
go printNumber(&wg, i)
}
wg.Wait() // 等待所有 Goroutine 完成
fmt.Println("All Goroutines finished.")
}
输出示例:
Number: 0
Number: 1
Number: 2
Number: 3
Number: 4
All Goroutines finished.
说明:
WaitGroup确保了所有子 Goroutine 完成后,主 Goroutine 才会输出 "All Goroutines finished." 并退出程序。相比简单的time.Sleep延时,WaitGroup是一种更可靠的 Goroutine 同步机制。
总结
- Goroutine 的非确定性:Go 中并发 Goroutine 的执行顺序无法确定,每次运行输出顺序可能不同。
- 闭包中的参数捕获问题:在闭包中直接使用循环变量可能导致所有 Goroutine 获取同一变量的最终值。解决方法是将循环变量作为参数传递。
- sync.Mutex 互斥锁:通过互斥锁实现并发安全,确保每次只有一个 Goroutine 能修改共享数据,防止数据竞争。
- sync.WaitGroup 同步机制:WaitGroup 用于等待一组 Goroutine 完成,是 Goroutine 同步的一种有效方式。
通过这些工具,可以有效地控制 Go 程序中的并发行为,实现更高效、更安全的并发操作。