24·并发编程进阶

sync 包与互斥锁

syncmutexrwmutexlock

Go 并发编程 - 第 24 课:sync 包与互斥锁

1. 学习目标

  • 理解为什么在并发编程中需要使用互斥锁(Mutex)
  • 掌握 sync.Mutexsync.RWMutex 的使用方法
  • 学会使用锁保护共享资源,避免数据竞争(Data Race)
  • 了解互斥锁的常见使用场景和最佳实践

2. 核心概念

2.1 为什么需要互斥锁?

在并发编程中,当多个 Goroutine 同时读写同一个变量时,可能会出现数据竞争。就像多个人同时修改同一个文件,结果会变得混乱。

问题示例

package main

import (
    "fmt"
    "sync"
)

func main() {
    counter := 0
    var wg sync.WaitGroup

    // 启动1000个Goroutine,每个增加counter
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter++ // 这里存在数据竞争!
        }()
    }

    wg.Wait()
    fmt.Println("最终结果:", counter) // 结果可能不是1000
}

2.2 互斥锁(Mutex)

互斥锁是一种简单的锁机制:同一时间只允许一个 Goroutine 访问被保护的代码块

  • Lock():获取锁。如果锁已被占用,则阻塞等待
  • Unlock():释放锁

2.3 读写锁(RWMutex)

读写锁是更精细的锁控制:

  • 读锁(RLock/RUnlock):多个 Goroutine 可以同时读取
  • 写锁(Lock/Unlock):写操作时独占,读操作时共享

适用场景:读多写少的情况,如缓存系统、配置读取等。

3. 代码示例

3.1 使用 Mutex 保护计数器

package main

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

type SafeCounter struct {
    mu    sync.Mutex // 互斥锁
    count int        // 共享资源
}

// 增加计数器
func (c *SafeCounter) Increment() {
    c.mu.Lock()         // 获取锁
    defer c.mu.Unlock() // 函数结束后释放锁(安全做法)
    c.count++
}

// 获取当前值
func (c *SafeCounter) Get() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

func main() {
    counter := SafeCounter{}
    var wg sync.WaitGroup

    // 启动1000个Goroutine并发增加计数器
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter.Increment()
        }()
    }

    // 启动读取Goroutine
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            time.Sleep(10 * time.Millisecond)
            fmt.Printf("当前值: %d\n", counter.Get())
        }()
    }

    wg.Wait()
    fmt.Println("最终结果:", counter.Get()) // 总是1000
}

3.2 使用 RWMutex 实现缓存

package main

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

type Cache struct {
    mu   sync.RWMutex // 读写锁
    data map[string]string
}

// 读取缓存
func (c *Cache) Get(key string) (string, bool) {
    c.mu.RLock()         // 获取读锁
    defer c.mu.RUnlock() // 释放读锁
    val, ok := c.data[key]
    return val, ok
}

// 写入缓存
func (c *Cache) Set(key, value string) {
    c.mu.Lock()         // 获取写锁
    defer c.mu.Unlock() // 释放写锁
    c.data[key] = value
}

func main() {
    cache := Cache{
        data: make(map[string]string),
    }

    var wg sync.WaitGroup

    // 并发写入
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            key := fmt.Sprintf("key%d", id)
            value := fmt.Sprintf("value%d", id)
            cache.Set(key, value)
            fmt.Printf("写入: %s = %s\n", key, value)
            time.Sleep(50 * time.Millisecond)
        }(i)
    }

    // 并发读取
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            time.Sleep(100 * time.Millisecond) // 等待一些写入完成
            key := fmt.Sprintf("key%d", id%5)
            if val, ok := cache.Get(key); ok {
                fmt.Printf("读取: %s = %s\n", key, val)
            }
        }(i)
    }

    wg.Wait()
}

4. 实践练习

练习1:线程安全的字符串构建器

创建一个 SafeStringBuilder 结构体,包含以下方法:

  • Append(s string):安全地追加字符串
  • String() string:安全地获取当前字符串

要求:使用 sync.Mutex 保护所有操作。

预期输出

最终结果: abcdefghijklmnopqrstuvwxyz(26个字母,顺序可能不同但完整)

练习2:并发缓存系统

实现一个简单的并发缓存,支持:

  • 并发读写操作
  • 设置过期时间
  • 自动清理过期缓存

要求

  1. 使用 sync.RWMutex 保护缓存访问
  2. 使用 Goroutine 定期清理过期项
  3. 提供 Get, Set, Delete 方法

提示:可以使用 time.AfterFunc 或定时器实现自动清理。

5. 常见错误

  1. 忘记解锁:导致其他 Goroutine 永远阻塞

    func bad() {
        mutex.Lock()
        // 如果这里发生panic或提前return,锁不会释放
    }
    
    // 正确做法:始终使用defer
    func good() {
        mutex.Lock()
        defer mutex.Unlock()
    }
    
  2. 在持有锁时调用可能阻塞的函数

    // 危险!可能导致死锁
    mutex.Lock()
    ch <- data // 如果channel满,会阻塞
    mutex.Unlock()
    
  3. 复制互斥锁:Mutex 不能被复制

    type BadStruct struct {
        mu sync.Mutex
        data int
    }
    
    var s1 BadStruct
    s2 := s1 // 错误!复制了Mutex
    
  4. 读写锁使用不当

    // 错误:写锁内读取
    rwmu.Lock() // 写锁
    defer rwmu.Unlock()
    if rwmu.RTryLock() { // 错误尝试获取读锁
        // 这会导致死锁!
    }
    

6. 小结

关键要点:

  1. Mutex(互斥锁):保护临界区,同一时间只有一个 Goroutine 可以访问
  2. RWMutex(读写锁):读操作共享,写操作独占,适合读多写少场景
  3. 最佳实践
    • 使用 defer mutex.Unlock() 确保锁释放
    • 保持临界区尽可能小
    • 避免在持有锁时调用可能阻塞的函数
  4. 使用场景
    • 计数器、状态管理 → Mutex
    • 缓存、配置读取 → RWMutex

何时使用哪种锁?

  • Mutex:当需要保护简单的读写操作,或不确定访问模式时
  • RWMutex:当明确读取操作远多于写入操作时

下一步预告:

下一课将学习 sync.WaitGroup 和并发控制模式,掌握如何协调多个 Goroutine 的执行顺序和完成情况。

练习编辑器

go
Loading...

继续学习

完成本课后,建议继续学习下一课「WaitGroup 与并发控制」 以巩固所学知识。