Go 并发编程 | 豆包MarsCode AI刷题

62 阅读4分钟

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 同步机制。


总结

  1. Goroutine 的非确定性:Go 中并发 Goroutine 的执行顺序无法确定,每次运行输出顺序可能不同。
  2. 闭包中的参数捕获问题:在闭包中直接使用循环变量可能导致所有 Goroutine 获取同一变量的最终值。解决方法是将循环变量作为参数传递。
  3. sync.Mutex 互斥锁:通过互斥锁实现并发安全,确保每次只有一个 Goroutine 能修改共享数据,防止数据竞争。
  4. sync.WaitGroup 同步机制:WaitGroup 用于等待一组 Goroutine 完成,是 Goroutine 同步的一种有效方式。

通过这些工具,可以有效地控制 Go 程序中的并发行为,实现更高效、更安全的并发操作。