深入解析 Golang 并发编程中的同步机制:WaitGroup 与 Mutex 详解

文章目录

      • 一、简介
      • [二、WaitGroup 的使用](#二、WaitGroup 的使用)
        • [1. 什么是 WaitGroup?](#1. 什么是 WaitGroup?)
        • [2. 基本操作](#2. 基本操作)
        • [3. WaitGroup 示例](#3. WaitGroup 示例)
        • [4. 注意事项](#4. 注意事项)
      • [三、Mutex 的使用](#三、Mutex 的使用)
        • [1. 什么是 Mutex?](#1. 什么是 Mutex?)
        • [2. 基本操作](#2. 基本操作)
        • [3. Mutex 示例](#3. Mutex 示例)
      • 四、竞争条件示例与解决
        • [1. 竞争条件问题示例](#1. 竞争条件问题示例)
        • [2. 使用 Mutex 解决竞争条件](#2. 使用 Mutex 解决竞争条件)
      • [五、使用 RWMutex 实现读写锁](#五、使用 RWMutex 实现读写锁)
        • [RWMutex 示例](#RWMutex 示例)
      • 六、小结

一、简介

在 Golang 的并发编程中,sync 包提供了一些基本的同步原语,用于解决多个 Goroutine 之间的协调与资源共享问题。常用的工具包括:

  • WaitGroup:用于等待一组 Goroutine 完成任务。
  • Mutex:用于保护共享资源,避免多个 Goroutine 同时修改数据导致竞争条件(Race Condition)。

本篇博客将详细介绍 WaitGroupMutex 的用法,并通过示例帮助初学者理解它们在实际场景中的应用。


二、WaitGroup 的使用

1. 什么是 WaitGroup?

WaitGroup 是一个计数器,用于等待多个 Goroutine 完成任务。它可以让主 Goroutine 阻塞,直到所有子 Goroutine 完成工作。

2. 基本操作
  • Add(delta int):添加 delta 个 Goroutine 到计数器。
  • Done():每个 Goroutine 在完成任务后调用,减少计数器的值。
  • Wait():阻塞调用者,直到计数器变为 0。
3. WaitGroup 示例
go 复制代码
package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // 任务完成时调用 Done
    fmt.Printf("Worker %d starting...\n", id)
    time.Sleep(time.Second) // 模拟任务执行
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup // 创建 WaitGroup 实例

    for i := 1; i <= 3; i++ {
        wg.Add(1) // 每启动一个 Goroutine,计数器加 1
        go worker(i, &wg)
    }

    wg.Wait() // 等待所有 Goroutine 完成
    fmt.Println("All workers done!")
}

输出:

复制代码
Worker 1 starting...
Worker 2 starting...
Worker 3 starting...
Worker 1 done
Worker 2 done
Worker 3 done
All workers done!
4. 注意事项
  • 确保 Done 与 Add 匹配 :每次 Add 对应一次 Done,否则可能会导致 Wait 永远阻塞。
  • 并发调用 Add :如果需要在运行时动态启动 Goroutine,应确保 Add 操作发生在 Wait 之前。

三、Mutex 的使用

1. 什么是 Mutex?

Mutex (互斥锁)用于防止多个 Goroutine 同时访问共享资源,从而避免竞争条件。只有一个 Goroutine 能够在同一时间持有 Mutex,其它 Goroutine 必须等待。

2. 基本操作
  • Lock():获取锁。如果锁已被其他 Goroutine 持有,则阻塞。
  • Unlock():释放锁。
3. Mutex 示例
go 复制代码
package main

import (
    "fmt"
    "sync"
    "time"
)

var (
    counter int          // 共享变量
    mutex   sync.Mutex   // 互斥锁保护共享变量
)

func increment(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 0; i < 5; i++ {
        mutex.Lock()   // 获取锁
        counter++      // 修改共享变量
        fmt.Printf("Goroutine %d incremented counter to %d\n", id, counter)
        mutex.Unlock() // 释放锁
        time.Sleep(time.Millisecond * 100) // 模拟其他操作
    }
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go increment(i, &wg)
    }

    wg.Wait()
    fmt.Println("Final Counter Value:", counter)
}

输出(示例):

复制代码
Goroutine 1 incremented counter to 1
Goroutine 2 incremented counter to 2
Goroutine 3 incremented counter to 3
...
Final Counter Value: 15

四、竞争条件示例与解决

1. 竞争条件问题示例

如果不使用 Mutex,多个 Goroutine 同时修改共享变量时会产生不一致的数据

go 复制代码
package main

import (
    "fmt"
    "sync"
    "time"
)

var counter int // 共享变量

func increment(wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 0; i < 5; i++ {
        counter++
        time.Sleep(time.Millisecond * 100) // 模拟其他操作
    }
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go increment(&wg)
    }

    wg.Wait()
    fmt.Println("Final Counter Value:", counter)
}

输出:

复制代码
Final Counter Value: 12

由于多个 Goroutine 竞争访问 counter,导致最终值不正确。


2. 使用 Mutex 解决竞争条件
go 复制代码
mutex.Lock()
counter++
mutex.Unlock()

通过为每次修改操作加锁,确保同一时间只有一个 Goroutine 可以修改 counter,从而避免数据不一致。


五、使用 RWMutex 实现读写锁

RWMutex 是一种读写锁,允许多个 Goroutine 同时读取,但在写入时需要独占锁。

RWMutex 示例
go 复制代码
package main

import (
    "fmt"
    "sync"
    "time"
)

var (
    data  = 0
    rwMux sync.RWMutex
)

func readData(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    rwMux.RLock() // 获取读锁
    fmt.Printf("Goroutine %d reading data: %d\n", id, data)
    rwMux.RUnlock() // 释放读锁
}

func writeData(wg *sync.WaitGroup) {
    defer wg.Done()
    rwMux.Lock() // 获取写锁
    data++
    fmt.Printf("Writing data: %d\n", data)
    rwMux.Unlock() // 释放写锁
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go readData(i, &wg)
    }

    wg.Add(1)
    go writeData(&wg)

    wg.Wait()
}

六、小结

  1. WaitGroup 用于等待一组 Goroutine 完成任务,是 Goroutine 同步的常用工具。
  2. MutexRWMutex 用于保护共享资源,避免竞争条件。
  3. 使用锁时需注意避免死锁性能瓶颈

在下一篇博客中,我们将介绍 Golang 的 Context 包,并探讨如何在并发操作中控制超时和取消任务。敬请期待!

相关推荐
David爱编程10 分钟前
Java 守护线程 vs 用户线程:一文彻底讲透区别与应用
java·后端
小奏技术28 分钟前
国内APP的隐私进步,从一个“营销授权”弹窗说起
后端·产品
小研说技术1 小时前
Spring AI存储向量数据
后端
苏三的开发日记1 小时前
jenkins部署ruoyi后台记录(jenkins与ruoyi后台处于同一台服务器)
后端
苏三的开发日记1 小时前
jenkins部署ruoyi后台记录(jenkins与ruoyi后台不在同一服务器)
后端
陈三一1 小时前
MyBatis OGNL 表达式避坑指南
后端·mybatis
whitepure1 小时前
万字详解JVM
java·jvm·后端
我崽不熬夜1 小时前
Java的条件语句与循环语句:如何高效编写你的程序逻辑?
java·后端·java ee
我崽不熬夜2 小时前
Java中的String、StringBuilder、StringBuffer:究竟该选哪个?
java·后端·java ee
文火冰糖的硅基工坊2 小时前
[激光原理与应用-317]:光学设计 - Solidworks - 草图
开发语言·python·信息可视化·系统架构