【Go语言学习系列31】并发编程(四):sync包

Go语言sync包并发编程详解

📚 原创系列: “Go语言学习系列”

🔄 转载说明: 本文最初发布于"Gopher部落"微信公众号,经原作者授权转载。

🔗 关注原创: 欢迎扫描文末二维码,关注"Gopher部落"微信公众号获取第一手Go技术文章。

📑 Go语言学习系列导航

本文是【Go语言学习系列】的第31篇,当前位于第三阶段(进阶篇)

🚀 第三阶段:进阶篇
  1. 并发编程(一):goroutine基础
  2. 并发编程(二):channel基础
  3. 并发编程(三):select语句
  4. 并发编程(四):sync包 👈 当前位置
  5. 并发编程(五):并发模式
  6. 并发编程(六):原子操作与内存模型
  7. 数据库编程(一):SQL接口
  8. 数据库编程(二):ORM技术
  9. Web开发(一):路由与中间件
  10. Web开发(二):模板与静态资源
  11. Web开发(三):API开发
  12. Web开发(四):认证与授权
  13. Web开发(五):WebSocket
  14. 微服务(一):基础概念
  15. 微服务(二):gRPC入门
  16. 日志与监控
  17. 第三阶段项目实战:微服务聊天应用

📚 查看完整Go语言学习系列导航

📖 文章导读

在本文中,您将了解:

  • Go语言sync包提供的各种同步原语及其使用场景
  • 互斥锁(Mutex)和读写锁(RWMutex)的正确使用方法
  • WaitGroup、Once和Cond等工具的实际应用
  • Map和Pool等并发安全的数据结构
  • 使用sync包的最佳实践和常见陷阱

虽然Go推崇"通过通信来共享内存"的CSP模型,但在某些场景下,使用传统的同步原语控制共享资源访问更加直接高效。本文将帮助你掌握sync包中的工具,以便在适当的场景下选择正确的并发控制机制。

Go sync包示意图

并发编程(四):sync包

在前面的文章中,我们深入探讨了Go语言的并发基础:goroutine、channel和select语句。这些机制主要基于CSP(通信顺序进程)模型,强调"通过通信来共享内存"。然而,在某些情况下,我们需要直接控制对共享资源的访问。这时,Go语言标准库中的sync包就派上用场了。

本文将详细介绍sync包中提供的同步原语,帮助你在适当的场景下选择正确的同步工具。

一、Mutex与RWMutex

互斥锁(Mutex)和读写锁(RWMutex)是sync包中最常用的同步原语,用于保护共享资源免受并发访问的干扰。

1.1 Mutex(互斥锁)

Mutex提供了一种互斥机制,确保同一时间只有一个goroutine可以访问共享资源。

基本用法

package main

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

func main() {
   
   
    var mutex sync.Mutex
    counter := 0
    
    // 并发更新counter
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
   
   
        wg.Add(1)
        go func() {
   
   
            defer wg.Done()
            
            mutex.Lock()         // 加锁
            defer mutex.Unlock() // 解锁
            
            counter++
        }()
    }
    
    wg.Wait()
    fmt.Println("计数器最终值:", counter) // 输出: 计数器最终值: 1000
}

重要方法

  • Lock():获取锁。如果锁已被其他goroutine获取,则阻塞直到锁可用
  • Unlock():释放锁。应在与Lock()相同的goroutine中调用
  • TryLock():(Go 1.18+)尝试获取锁,如果锁不可用则立即返回false而不阻塞

最佳实践

  1. 总是使用defer mutex.Unlock()确保锁被释放
  2. 尽量减小临界区(加锁和解锁之间的代码)
  3. 避免在持有锁的情况下调用可能阻塞的操作
  4. 不要在goroutine A中锁定,然后在goroutine B中解锁

常见错误

// 错误1: 忘记解锁
func increment(counter *int, mu *sync.Mutex) {
   
   
    mu.Lock()
    *counter++
    // 忘记调用mu.Unlock(),导致死锁
}

// 错误2: 重复解锁
func decrement(counter *int, mu *sync.Mutex) {
   
   
    mu.Lock()
    *counter--
    mu.Unlock()
    mu.Unlock() // 错误: 再次解锁已经解锁的互斥量会导致panic
}

// 错误3: 复制互斥锁
func copyMutex() {
   
   
    var mu sync.Mutex
    mu.Lock()
    
    muCopy := mu // 错误: 复制了互斥锁
    muCopy.Unlock() // 未定义行为
}

1.2 RWMutex(读写锁)

RWMutex允许多个读操作并发执行,但写操作是互斥的。当有写锁时,所有的读操作都会被阻塞。

基本用法

package main

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

func main() {
   
   
    var rwMutex sync.RWMutex
    data := make(map[string]string)
    
    // 写入操作
    go func() {
   
   
        for i := 0; i < 10; i++ {
   
   
            rwMutex.Lock() // 写锁
            key := fmt.Sprintf("key%d", i)
            data[key] = fmt.Sprintf("value%d", i)
            time.Sleep(100 * time.Millisecond) // 模拟写入耗时
            rwMutex.Unlock()
            time.Sleep(200 * time.Millisecond) // 给读操作时间
        }
    }()
    
    // 多个并发读取操作
    var wg sync.WaitGroup
    for r := 0; r < 5; r++ {
   
   
        wg.Add(1)
        go func(id int) {
   
   
            defer wg.Done()
            
            for i := 0; i < 10; i++ {
   
   
                rwMutex.RLock() // 读锁
                for k, v := range data {
   
   
                    fmt.Printf("读取者 %d: %s = %s\n", id, k, v)
                }
                rwMutex.RUnlock()
                time.Sleep(150 * time.Millisecond)
            }
        }(r)
    }
    
    wg.Wait()
}

重要方法

  • Lock()/Unlock():获取/释放写锁。与Mutex相同,写操作是互斥的
  • RLock()/RUnlock():获取/释放读锁。多个goroutine可以同时持有读锁
  • TryLock()/TryRLock():(Go 1.18+)尝试获取写锁/读锁,非阻塞

使用场景

当共享资源的读操作远多于写操作时,RWMutex比Mutex更有效。例如:

  • 配置信息:频繁读取,偶尔更新
  • 缓存系统:大量读取,少量写入
  • 统计数据:持续读取,定期更新

性能考虑

  • 如果读写比例接近1:1,普通的Mutex可能更高效
  • RWMutex有额外的开销来跟踪读锁
  • 写锁会阻塞所有的新读锁请求,可能导致"写饥饿"

二、WaitGroup

WaitGroup用于等待一组goroutine完成执行。它提供了一种简单的方式来协调多个并发操作的完成。

2.1 基本用法

WaitGroup的使用非常直观:

package main

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

func worker(id int, wg *sync.WaitGroup) {
   
   
    defer wg.Done() // 工作完成时通知WaitGroup
    
    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(time.Second) // 模拟工作
    fmt.Printf("Worker %d done\n", id)
}

func main()
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Gopher部落

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值