Skip to content

互斥锁 Mutex

1. 概述

互斥锁(Mutex)是 Go 语言中最基本的同步原语,用于保护共享资源,确保同一时刻只有一个 Goroutine 可以访问共享资源。Mutex 是 sync 包中的一个结构体,提供了简单而有效的并发控制机制。

在整个 Go 语言课程体系中,Mutex 是并发编程的基础组件之一,与 Goroutine、通道一起构成了 Go 语言并发模型的核心。掌握 Mutex 的使用和原理,对于构建可靠、高效的并发系统至关重要。

2. 基本概念

2.1 语法

2.1.1 基本用法

go
import "sync"

// 创建互斥锁
var mu sync.Mutex

// 加锁
mu.Lock()
// 访问共享资源
// ...
// 解锁
mu.Unlock()

// 或者使用 defer 解锁
mu.Lock()
defer mu.Unlock()
// 访问共享资源
// ...

2.1.2 示例代码

go
package main

import (
    "fmt"
    "sync"
)

func main() {
    var mu sync.Mutex
    counter := 0
    var wg sync.WaitGroup
    
    // 1000 个 Goroutine 同时递增计数器
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            mu.Lock()
            defer mu.Unlock()
            counter++
        }()
    }
    
    wg.Wait()
    fmt.Printf("Final counter value: %d\n", counter) // 应该输出 1000
}

2.2 语义

  • Lock():获取锁,如果锁已经被其他 Goroutine 持有,则阻塞直到获取到锁。
  • Unlock():释放锁,唤醒等待锁的 Goroutine。
  • 零值可用:Mutex 的零值是可用的,不需要初始化。
  • 不可复制:Mutex 是结构体,不是引用类型,不要复制使用中的 Mutex。
  • 不可重入:同一个 Goroutine 不能多次获取同一个 Mutex,否则会导致死锁。

2.3 规范

  • 命名规范:Mutex 变量通常命名为 mu
  • 使用顺序:总是先加锁,后访问共享资源,最后解锁。
  • defer 解锁:使用 defer mu.Unlock() 确保即使发生 panic 也能正确解锁。
  • 锁粒度:尽量减少持有锁的时间,只在必要时加锁。
  • 避免嵌套锁:避免在持有一个锁的同时获取另一个锁,减少死锁的风险。
  • 不可复制:通过指针传递 Mutex,避免复制它。

3. 原理深度解析

3.1 Mutex 结构体

Mutex 的底层实现是一个结构体,在 Go 1.18+ 版本中,其结构如下:

go
type Mutex struct {
    state int32
    sema  uint32
}

其中:

  • state:表示锁的状态,包含以下信息:
    • 锁是否被持有(bit 0)
    • 是否有 Goroutine 在等待锁(bit 1)
    • 等待锁的 Goroutine 数量(bits 2-30)
  • sema:信号量,用于唤醒等待的 Goroutine。

3.2 锁的状态转换

Mutex 有两种模式:

  1. 正常模式:新请求锁的 Goroutine 会先尝试自旋(spin)一段时间,如果获取不到锁则进入等待队列。
  2. 饥饿模式:当一个 Goroutine 等待锁的时间超过 1ms 时,Mutex 会切换到饥饿模式,此时锁会直接传递给等待时间最长的 Goroutine。

3.3 Lock 方法实现

Lock 方法的主要步骤:

  1. 快速路径:使用 CAS(Compare-And-Swap)操作尝试获取锁,如果成功则直接返回。
  2. 自旋:如果锁被占用,且当前 Goroutine 是第一个等待者,且锁的持有者在运行中,则进行短暂的自旋。
  3. 慢速路径:如果自旋失败,则进入慢速路径,将自己加入等待队列,然后阻塞等待。
  4. 模式切换:如果等待时间超过 1ms,切换到饥饿模式。

3.4 Unlock 方法实现

Unlock 方法的主要步骤:

  1. 快速路径:使用 CAS 操作释放锁,如果没有等待者则直接返回。
  2. 唤醒等待者:如果有等待者,则唤醒队列中的第一个等待者。
  3. 模式切换:如果锁的持有者是通过正常模式获取的,且等待队列不为空,则切换回正常模式。

3.5 并发安全

Mutex 的所有方法都是并发安全的,使用原子操作来修改状态,确保在多 Goroutine 环境中安全使用。

3.6 内存模型

Mutex 遵循 Go 语言的内存模型,确保以下顺序:

  • 在调用 Unlock() 之前的所有操作,发生在 Lock() 返回之后。
  • 多个 Goroutine 同时调用 Lock() 时,只有一个 Goroutine 会成功获取锁,其他 Goroutine 会等待。

4. 常见错误与踩坑点

4.1 死锁

错误表现:程序卡住,无法继续执行。

产生原因

  • 同一 Goroutine 多次获取同一个 Mutex。
  • 多个 Goroutine 循环等待对方释放锁。
  • 锁的顺序不一致,导致循环等待。

解决方案

  • 确保每个锁都能正确释放,使用 defer 语句。
  • 保持一致的锁获取顺序。
  • 避免在持有锁时调用可能阻塞的函数。
go
// 错误示例:同一 Goroutine 多次获取同一个 Mutex
func main() {
    var mu sync.Mutex
    mu.Lock()
    mu.Lock() // 死锁
    defer mu.Unlock()
    defer mu.Unlock()
}

// 错误示例:循环等待
var mu1, mu2 sync.Mutex

func goroutine1() {
    mu1.Lock()
    defer mu1.Unlock()
    time.Sleep(time.Millisecond)
    mu2.Lock()
    defer mu2.Unlock()
}

func goroutine2() {
    mu2.Lock()
    defer mu2.Unlock()
    time.Sleep(time.Millisecond)
    mu1.Lock()
    defer mu1.Unlock()
}

// 正确示例:一致的锁顺序
func process() {
    // 始终先获取 mu1,再获取 mu2
    mu1.Lock()
    defer mu1.Unlock()
    mu2.Lock()
    defer mu2.Unlock()
    // 处理逻辑
}

4.2 忘记解锁

错误表现:其他 Goroutine 无法获取锁,导致死锁。

产生原因:在获取锁后,没有对应的解锁操作,或者在解锁前发生了 panic。

解决方案:始终使用 defer 语句来确保解锁,即使发生 panic 也能正确解锁。

go
// 错误示例:忘记解锁
func process() {
    mu.Lock()
    // 处理逻辑
    // 忘记调用 mu.Unlock()
}

// 正确示例:使用 defer 解锁
func process() {
    mu.Lock()
    defer mu.Unlock()
    // 处理逻辑
}

4.3 锁粒度太大

错误表现:并发性能下降,锁竞争严重。

产生原因:长时间持有锁,执行了很多不需要锁保护的操作。

解决方案:尽量减少持有锁的时间,只在必要时加锁。

go
// 错误示例:锁粒度太大
func process() {
    mu.Lock()
    defer mu.Unlock()
    // 执行耗时操作
    time.Sleep(time.Second)
    // 修改共享资源
    sharedResource = newValue
}

// 正确示例:锁粒度最小化
func process() {
    // 执行耗时操作
    time.Sleep(time.Second)
    // 只在修改共享资源时加锁
    mu.Lock()
    sharedResource = newValue
    mu.Unlock()
}

4.4 复制 Mutex

错误表现:复制的 Mutex 与原 Mutex 状态不同步,导致不可预期的行为。

产生原因:Mutex 是结构体,不是引用类型,复制后会创建一个新的实例,与原实例状态无关。

解决方案:通过指针传递 Mutex,而不是复制它。

go
// 错误示例:复制 Mutex
func worker(mu sync.Mutex) { // 复制 Mutex
    mu.Lock()
    defer mu.Unlock()
    // 处理逻辑
}

// 正确示例:通过指针传递
func worker(mu *sync.Mutex) { // 通过指针传递
    mu.Lock()
    defer mu.Unlock()
    // 处理逻辑
}

4.5 锁的使用不当导致性能问题

错误表现:程序运行缓慢,并发性能差。

产生原因

  • 锁的粒度太大,导致锁竞争严重。
  • 在持有锁时执行 I/O 操作或其他阻塞操作。
  • 不必要的锁使用,如对只读操作加锁。

解决方案

  • 最小化锁的粒度,只在必要时加锁。
  • 避免在持有锁时执行阻塞操作。
  • 对于读多写少的场景,使用 RWMutex 提高并发性能。
go
// 错误示例:在持有锁时执行 I/O 操作
func process() {
    mu.Lock()
    defer mu.Unlock()
    // 执行 I/O 操作
    data, err := ioutil.ReadFile("file.txt")
    if err != nil {
        return
    }
    // 处理数据
    sharedResource = processData(data)
}

// 正确示例:先执行 I/O 操作,再获取锁
func process() {
    // 执行 I/O 操作
    data, err := ioutil.ReadFile("file.txt")
    if err != nil {
        return
    }
    // 处理数据
    processedData := processData(data)
    // 只在修改共享资源时加锁
    mu.Lock()
    sharedResource = processedData
    mu.Unlock()
}

5. 常见应用场景

5.1 保护共享变量

场景描述:多个 Goroutine 需要访问和修改同一个共享变量,如计数器、配置等。

使用方法:使用 Mutex 保护共享变量,确保同一时刻只有一个 Goroutine 可以修改它。

示例代码

go
package main

import (
    "fmt"
    "sync"
)

func main() {
    var mu sync.Mutex
    counter := 0
    var wg sync.WaitGroup
    
    // 1000 个 Goroutine 同时递增计数器
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            mu.Lock()
            defer mu.Unlock()
            counter++
        }()
    }
    
    wg.Wait()
    fmt.Printf("Final counter value: %d\n", counter) // 应该输出 1000
}

5.2 保护共享数据结构

场景描述:多个 Goroutine 需要访问和修改同一个共享数据结构,如 map、slice 等。

使用方法:使用 Mutex 保护共享数据结构,确保同一时刻只有一个 Goroutine 可以修改它。

示例代码

go
package main

import (
    "fmt"
    "sync"
)

func main() {
    var mu sync.Mutex
    data := make(map[string]int)
    var wg sync.WaitGroup
    
    // 10 个 Goroutine 同时修改 map
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            key := fmt.Sprintf("key-%d", id)
            mu.Lock()
            defer mu.Unlock()
            data[key] = id
            fmt.Printf("Added %s: %d\n", key, id)
        }(i)
    }
    
    wg.Wait()
    fmt.Println("Final data:")
    for k, v := range data {
        fmt.Printf("%s: %d\n", k, v)
    }
}

5.3 实现线程安全的结构体

场景描述:需要实现一个线程安全的结构体,支持并发访问和修改。

使用方法:在结构体中嵌入 Mutex,在方法中使用 Mutex 保护共享资源。

示例代码

go
package main

import (
    "fmt"
    "sync"
)

type Counter struct {
    mu    sync.Mutex
    value int
}

func (c *Counter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value++
}

func (c *Counter) Get() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.value
}

func main() {
    counter := &Counter{}
    var wg sync.WaitGroup
    
    // 1000 个 Goroutine 同时递增计数器
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter.Increment()
        }()
    }
    
    wg.Wait()
    fmt.Printf("Final counter value: %d\n", counter.Get()) // 应该输出 1000
}

5.4 实现临界区

场景描述:需要确保一段代码在同一时刻只有一个 Goroutine 执行,如初始化操作、资源清理等。

使用方法:使用 Mutex 保护临界区,确保同一时刻只有一个 Goroutine 执行这段代码。

示例代码

go
package main

import (
    "fmt"
    "sync"
)

func main() {
    var mu sync.Mutex
    var initialized bool
    var wg sync.WaitGroup
    
    // 10 个 Goroutine 同时尝试初始化
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            mu.Lock()
            defer mu.Unlock()
            if !initialized {
                fmt.Printf("Goroutine %d: initializing\n", id)
                initialized = true
                fmt.Printf("Goroutine %d: initialization completed\n", id)
            } else {
                fmt.Printf("Goroutine %d: already initialized\n", id)
            }
        }(i)
    }
    
    wg.Wait()
    fmt.Println("All goroutines completed")
}

5.5 实现单例模式

场景描述:需要创建一个全局唯一的实例,确保只初始化一次。

使用方法:使用 Mutex 保护单例的初始化过程,确保只初始化一次。

示例代码

go
package main

import (
    "fmt"
    "sync"
)

type Singleton struct {
    data string
}

var (
    instance *Singleton
    mu       sync.Mutex
)

func GetInstance() *Singleton {
    mu.Lock()
    defer mu.Unlock()
    if instance == nil {
        instance = &Singleton{data: "initialized"}
        fmt.Println("Singleton initialized")
    }
    return instance
}

// 优化版本:双重检查锁定
func GetInstanceOptimized() *Singleton {
    if instance == nil {
        mu.Lock()
        defer mu.Unlock()
        if instance == nil {
            instance = &Singleton{data: "initialized"}
            fmt.Println("Singleton initialized")
        }
    }
    return instance
}

func main() {
    var wg sync.WaitGroup
    
    // 多个 Goroutine 同时获取单例实例
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            s := GetInstanceOptimized()
            fmt.Printf("Goroutine %d: %s\n", id, s.data)
        }(i)
    }
    
    wg.Wait()
    fmt.Println("All goroutines completed")
}

6. 企业级进阶应用场景

6.1 并发安全的配置管理

场景描述:在大型应用中,需要管理全局配置,支持并发读写和热更新。

使用方法:使用 Mutex 保护配置,确保配置的一致性。

示例代码

go
package config

import (
    "sync"
)

type Config struct {
    Server struct {
        Host string
        Port int
    }
    Database struct {
        DSN string
    }
    // 其他配置项
}

var (
    config Config
    mu     sync.Mutex
)

func GetConfig() Config {
    mu.Lock()
    defer mu.Unlock()
    return config
}

func UpdateConfig(newConfig Config) {
    mu.Lock()
    defer mu.Unlock()
    config = newConfig
}

// 热更新配置
func ReloadConfig() error {
    // 从文件或环境变量加载配置
    newConfig, err := loadConfig()
    if err != nil {
        return err
    }
    UpdateConfig(newConfig)
    return nil
}

6.2 并发安全的缓存

场景描述:在高并发系统中,需要一个线程安全的缓存,支持并发读写。

使用方法:使用 Mutex 保护缓存,确保缓存的一致性。

示例代码

go
package cache

import (
    "sync"
    "time"
)

type Item struct {
    Value      interface{}
    Expiration int64
}

type Cache struct {
    items map[string]Item
    mu    sync.Mutex
}

func NewCache() *Cache {
    c := &Cache{
        items: make(map[string]Item),
    }
    // 启动清理过期项的 Goroutine
    go c.cleanup()
    return c
}

func (c *Cache) Set(key string, value interface{}, expiration time.Duration) {
    c.mu.Lock()
    defer c.mu.Unlock()
    var exp int64
    if expiration > 0 {
        exp = time.Now().Add(expiration).UnixNano()
    }
    c.items[key] = Item{
        Value:      value,
        Expiration: exp,
    }
}

func (c *Cache) Get(key string) (interface{}, bool) {
    c.mu.Lock()
    defer c.mu.Unlock()
    item, found := c.items[key]
    if !found {
        return nil, false
    }
    if item.Expiration > 0 && time.Now().UnixNano() > item.Expiration {
        delete(c.items, key)
        return nil, false
    }
    return item.Value, true
}

func (c *Cache) Delete(key string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    delete(c.items, key)
}

func (c *Cache) cleanup() {
    for {
        time.Sleep(time.Minute)
        c.mu.Lock()
        now := time.Now().UnixNano()
        for k, v := range c.items {
            if v.Expiration > 0 && now > v.Expiration {
                delete(c.items, k)
            }
        }
        c.mu.Unlock()
    }
}

6.3 并发安全的队列

场景描述:在生产者-消费者模式中,需要一个线程安全的队列,支持并发入队和出队。

使用方法:使用 Mutex 保护队列,确保队列的一致性。

示例代码

go
package queue

import (
    "sync"
)

type Queue struct {
    items []interface{}
    mu    sync.Mutex
}

func NewQueue() *Queue {
    return &Queue{
        items: make([]interface{}, 0),
    }
}

func (q *Queue) Enqueue(item interface{}) {
    q.mu.Lock()
    defer q.mu.Unlock()
    q.items = append(q.items, item)
}

func (q *Queue) Dequeue() (interface{}, bool) {
    q.mu.Lock()
    defer q.mu.Unlock()
    if len(q.items) == 0 {
        return nil, false
    }
    item := q.items[0]
    q.items = q.items[1:]
    return item, true
}

func (q *Queue) Size() int {
    q.mu.Lock()
    defer q.mu.Unlock()
    return len(q.items)
}

6.4 并发安全的计数器

场景描述:在分布式系统中,需要一个高并发的计数器,支持原子操作。

使用方法:使用 Mutex 保护计数器,确保计数器的一致性。

示例代码

go
package counter

import (
    "sync"
)

type Counter struct {
    value int64
    mu    sync.Mutex
}

func NewCounter() *Counter {
    return &Counter{value: 0}
}

func (c *Counter) Increment() int64 {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value++
    return c.value
}

func (c *Counter) Decrement() int64 {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value--
    return c.value
}

func (c *Counter) Get() int64 {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.value
}

func (c *Counter) Set(value int64) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value = value
}

6.5 并发安全的连接池

场景描述:在高并发系统中,需要管理数据库连接、网络连接等资源,避免频繁创建和销毁连接。

使用方法:使用 Mutex 保护连接池,确保连接的安全获取和归还。

示例代码

go
package pool

import (
    "sync"
)

type Connection interface {
    Close() error
}

type Pool struct {
    connections chan Connection
    mu          sync.Mutex
    closed      bool
}

func NewPool(size int, factory func() (Connection, error)) (*Pool, error) {
    pool := &Pool{
        connections: make(chan Connection, size),
        closed:      false,
    }
    
    // 预创建连接
    for i := 0; i < size; i++ {
        conn, err := factory()
        if err != nil {
            return nil, err
        }
        pool.connections <- conn
    }
    
    return pool, nil
}

func (p *Pool) Get() (Connection, error) {
    p.mu.Lock()
    if p.closed {
        p.mu.Unlock()
        return nil, fmt.Errorf("pool is closed")
    }
    p.mu.Unlock()
    
    conn := <-p.connections
    return conn, nil
}

func (p *Pool) Put(conn Connection) error {
    p.mu.Lock()
    defer p.mu.Unlock()
    
    if p.closed {
        return conn.Close()
    }
    
    select {
    case p.connections <- conn:
        return nil
    default:
        // 连接池已满,关闭多余的连接
        return conn.Close()
    }
}

func (p *Pool) Close() error {
    p.mu.Lock()
    defer p.mu.Unlock()
    
    if p.closed {
        return nil
    }
    
    p.closed = true
    close(p.connections)
    
    for conn := range p.connections {
        conn.Close()
    }
    
    return nil
}

func (p *Pool) Size() int {
    p.mu.Lock()
    defer p.mu.Unlock()
    return len(p.connections)
}

7. 行业最佳实践

7.1 始终使用 defer 解锁

实践内容:使用 defer 语句确保锁的释放。

推荐理由defer 语句可以确保即使发生 panic,锁也能正确释放,避免死锁。

示例

go
func process() {
    mu.Lock()
    defer mu.Unlock()
    // 处理逻辑
}

7.2 最小化锁的粒度

实践内容:只对需要保护的共享资源加锁,避免对整个函数加锁。

推荐理由:最小化锁的粒度可以提高并发性能,减少锁竞争。

示例

go
// 错误示例:对整个函数加锁
func process() {
    mu.Lock()
    defer mu.Unlock()
    // 执行不需要锁的操作
    fmt.Println("Processing...")
    // 修改共享资源
    sharedResource = newValue
}

// 正确示例:只对共享资源加锁
func process() {
    // 执行不需要锁的操作
    fmt.Println("Processing...")
    // 只对共享资源加锁
    mu.Lock()
    sharedResource = newValue
    mu.Unlock()
}

7.3 避免在持有锁时执行阻塞操作

实践内容:避免在持有锁时执行 I/O 操作、网络请求等阻塞操作。

推荐理由:在持有锁时执行阻塞操作会增加锁的持有时间,降低并发性能,增加死锁的风险。

示例

go
// 错误示例:在持有锁时执行 I/O 操作
func process() {
    mu.Lock()
    defer mu.Unlock()
    // 执行 I/O 操作
    data, err := ioutil.ReadFile("file.txt")
    if err != nil {
        return
    }
    // 处理数据
    sharedResource = processData(data)
}

// 正确示例:先执行 I/O 操作,再获取锁
func process() {
    // 执行 I/O 操作
    data, err := ioutil.ReadFile("file.txt")
    if err != nil {
        return
    }
    // 处理数据
    processedData := processData(data)
    // 只在修改共享资源时加锁
    mu.Lock()
    sharedResource = processedData
    mu.Unlock()
}

7.4 使用双重检查锁定模式

实践内容:对于单例模式等场景,使用双重检查锁定模式提高性能。

推荐理由:双重检查锁定模式可以减少锁的竞争,提高性能。

示例

go
func GetInstance() *Singleton {
    if instance == nil {
        mu.Lock()
        defer mu.Unlock()
        if instance == nil {
            instance = &Singleton{data: "initialized"}
        }
    }
    return instance
}

7.5 保持一致的锁获取顺序

实践内容:当需要获取多个锁时,保持一致的获取顺序。

推荐理由:保持一致的锁获取顺序可以避免死锁。

示例

go
// 正确示例:保持一致的锁顺序
func process() {
    // 始终先获取 mu1,再获取 mu2
    mu1.Lock()
    defer mu1.Unlock()
    mu2.Lock()
    defer mu2.Unlock()
    // 处理逻辑
}

7.6 避免嵌套锁

实践内容:避免在持有一个锁的同时获取另一个锁。

推荐理由:嵌套锁容易导致死锁,尤其是当多个 Goroutine 以不同的顺序获取锁时。

示例

go
// 错误示例:嵌套锁
func process() {
    mu1.Lock()
    defer mu1.Unlock()
    // ...
    mu2.Lock()
    defer mu2.Unlock()
    // ...
}

// 正确示例:避免嵌套锁
func process() {
    // 先获取所有需要的锁
    mu1.Lock()
    mu2.Lock()
    // 处理逻辑
    mu2.Unlock()
    mu1.Unlock()
}

7.7 监控锁的使用

实践内容:在生产环境中监控锁的使用情况,如锁的持有时间、竞争情况等。

推荐理由:监控锁的使用情况可以帮助发现潜在的性能问题和死锁风险。

示例

go
// 使用监控工具监控锁的使用
func process() {
    start := time.Now()
    mu.Lock()
    defer func() {
        mu.Unlock()
        duration := time.Since(start)
        metrics.RecordLockHoldTime("process", duration)
    }()
    // 处理逻辑
}

7.8 选择合适的同步原语

实践内容:根据具体场景选择合适的同步原语。

推荐理由:不同的同步原语有不同的适用场景,选择合适的同步原语可以提高性能和代码可读性。

示例

  • 对于读多写少的场景,使用 RWMutex。
  • 对于简单的计数器,使用 atomic 包。
  • 对于需要等待条件满足的场景,使用 Cond。
  • 对于需要等待多个 Goroutine 完成的场景,使用 WaitGroup。

8. 常见问题答疑(FAQ)

8.1 Mutex 和 RWMutex 有什么区别?

问题描述:Mutex 和 RWMutex 都是同步原语,它们有什么区别?

回答内容

  • Mutex:互斥锁,确保同一时刻只有一个 Goroutine 可以访问共享资源。
  • RWMutex:读写锁,允许多个读操作同时进行,但写操作会阻塞所有读写操作。
  • 使用场景
    • 当读写比例相近时,使用 Mutex。
    • 当读操作远多于写操作时,使用 RWMutex 可以提高并发性能。

示例代码

go
// 使用 Mutex
var mu sync.Mutex

// 使用 RWMutex
var rwmu sync.RWMutex

8.2 如何避免死锁?

问题描述:如何避免死锁?

回答内容

  • 始终使用 defer 语句确保锁的释放。
  • 保持一致的锁获取顺序。
  • 避免在持有锁时调用可能阻塞的函数。
  • 避免嵌套锁,或者确保以相同的顺序获取锁。
  • 使用超时机制,避免无限等待。
  • 定期检查代码中的死锁风险。

示例代码

go
// 避免死锁:保持一致的锁顺序
func process() {
    // 始终先获取 mu1,再获取 mu2
    mu1.Lock()
    defer mu1.Unlock()
    mu2.Lock()
    defer mu2.Unlock()
    // 处理逻辑
}

8.3 如何提高 Mutex 的性能?

问题描述:如何提高 Mutex 的性能?

回答内容

  • 最小化锁的粒度,只在必要时加锁。
  • 避免在持有锁时执行阻塞操作。
  • 使用双重检查锁定模式减少锁的竞争。
  • 对于读多写少的场景,使用 RWMutex。
  • 对于简单的计数器,使用 atomic 包。
  • 合理设计并发模型,减少锁的使用。

示例代码

go
// 提高性能:最小化锁的粒度
func process() {
    // 执行不需要锁的操作
    data := computeData()
    // 只在修改共享资源时加锁
    mu.Lock()
    sharedResource = data
    mu.Unlock()
}

8.4 Mutex 是可重入的吗?

问题描述:Mutex 是可重入的吗?

回答内容

  • 不是,Mutex 不是可重入的。
  • 同一个 Goroutine 不能多次获取同一个 Mutex,否则会导致死锁。
  • 如果需要可重入的锁,可以使用 sync.Mutex 的包装实现,或者使用其他同步原语。

示例代码

go
// 错误示例:Mutex 不可重入
func main() {
    var mu sync.Mutex
    mu.Lock()
    mu.Lock() // 死锁
    defer mu.Unlock()
    defer mu.Unlock()
}

8.5 如何处理 Mutex 的复制问题?

问题描述:如何处理 Mutex 的复制问题?

回答内容

  • Mutex 是结构体,不是引用类型,复制后会创建一个新的实例,与原实例状态无关。
  • 避免复制使用中的 Mutex,通过指针传递 Mutex。
  • 在结构体中嵌入 Mutex 时,注意不要复制整个结构体。

示例代码

go
// 错误示例:复制 Mutex
func worker(mu sync.Mutex) { // 复制 Mutex
    mu.Lock()
    defer mu.Unlock()
    // 处理逻辑
}

// 正确示例:通过指针传递
func worker(mu *sync.Mutex) { // 通过指针传递
    mu.Lock()
    defer mu.Unlock()
    // 处理逻辑
}

8.6 Mutex 和通道有什么区别?

问题描述:Mutex 和通道都是 Go 语言中用于并发控制的工具,它们有什么区别?

回答内容

  • Mutex:互斥锁,用于保护共享资源,确保同一时刻只有一个 Goroutine 可以访问共享资源。
  • 通道:用于 Goroutine 间的通信,可以传递数据和信号。
  • 使用场景
    • 当需要保护共享资源时,使用 Mutex。
    • 当需要在 Goroutine 间传递数据或信号时,使用通道。
    • 当需要更复杂的并发控制时,结合使用 Mutex 和通道。

示例代码

go
// 使用 Mutex 保护共享资源
var mu sync.Mutex
var counter int

func increment() {
    mu.Lock()
    defer mu.Unlock()
    counter++
}

// 使用通道进行通信
ch := make(chan int)

go func() {
    ch <- 42
}()

value := <-ch

9. 实战练习

9.1 基础练习:并发安全的计数器

题目:使用 Mutex 实现一个并发安全的计数器,支持递增、递减和获取操作。

解题思路

  • 使用 Mutex 保护计数器变量。
  • 实现 Increment、Decrement 和 Get 方法。
  • 测试多个 Goroutine 同时操作计数器。

常见误区

  • 忘记使用 defer 解锁,导致死锁。
  • 锁的粒度太大,导致性能下降。

分步提示

  1. 定义计数器结构体,包含计数器值和 Mutex。
  2. 实现 Increment 方法,使用 Mutex 保护计数器的递增操作。
  3. 实现 Decrement 方法,使用 Mutex 保护计数器的递减操作。
  4. 实现 Get 方法,使用 Mutex 保护计数器的读取操作。
  5. 启动多个 Goroutine 同时操作计数器。
  6. 验证计数器的最终值是否正确。

参考代码

go
package main

import (
    "fmt"
    "sync"
)

type Counter struct {
    value int
    mu    sync.Mutex
}

func (c *Counter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value++
}

func (c *Counter) Decrement() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value--
}

func (c *Counter) Get() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.value
}

func main() {
    counter := &Counter{}
    var wg sync.WaitGroup
    
    // 1000 个 Goroutine 同时递增计数器
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter.Increment()
        }()
    }
    
    // 500 个 Goroutine 同时递减计数器
    for i := 0; i < 500; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter.Decrement()
        }()
    }
    
    wg.Wait()
    fmt.Printf("Final counter value: %d\n", counter.Get()) // 应该输出 500
}

9.2 进阶练习:并发安全的缓存

题目:使用 Mutex 实现一个并发安全的缓存,支持设置、获取和删除操作,以及过期时间。

解题思路

  • 使用 Mutex 保护缓存映射。
  • 实现 Set、Get 和 Delete 方法。
  • 支持设置过期时间,定期清理过期项。
  • 测试多个 Goroutine 同时访问缓存。

常见误区

  • 在读操作时使用写锁定,导致性能下降。
  • 没有正确处理并发访问,导致竞态条件。
  • 没有清理过期项,导致缓存膨胀。

分步提示

  1. 定义缓存项结构体,包含值和过期时间。
  2. 定义缓存结构体,包含映射和 Mutex。
  3. 实现 Set 方法,使用 Mutex 保护缓存的设置操作。
  4. 实现 Get 方法,使用 Mutex 保护缓存的获取操作,并检查过期时间。
  5. 实现 Delete 方法,使用 Mutex 保护缓存的删除操作。
  6. 启动一个 Goroutine 定期清理过期项。
  7. 测试多个 Goroutine 同时访问缓存。

参考代码

go
package main

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

type Item struct {
    Value      interface{}
    Expiration int64
}

type Cache struct {
    items map[string]Item
    mu    sync.Mutex
}

func NewCache() *Cache {
    c := &Cache{
        items: make(map[string]Item),
    }
    // 启动清理过期项的 Goroutine
    go c.cleanup()
    return c
}

func (c *Cache) Set(key string, value interface{}, expiration time.Duration) {
    c.mu.Lock()
    defer c.mu.Unlock()
    var exp int64
    if expiration > 0 {
        exp = time.Now().Add(expiration).UnixNano()
    }
    c.items[key] = Item{
        Value:      value,
        Expiration: exp,
    }
}

func (c *Cache) Get(key string) (interface{}, bool) {
    c.mu.Lock()
    defer c.mu.Unlock()
    item, found := c.items[key]
    if !found {
        return nil, false
    }
    if item.Expiration > 0 && time.Now().UnixNano() > item.Expiration {
        delete(c.items, key)
        return nil, false
    }
    return item.Value, true
}

func (c *Cache) Delete(key string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    delete(c.items, key)
}

func (c *Cache) cleanup() {
    for {
        time.Sleep(time.Minute)
        c.mu.Lock()
        now := time.Now().UnixNano()
        for k, v := range c.items {
            if v.Expiration > 0 && now > v.Expiration {
                delete(c.items, k)
            }
        }
        c.mu.Unlock()
    }
}

func main() {
    cache := NewCache()
    var wg sync.WaitGroup
    
    // 启动 10 个写 Goroutine
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            for j := 0; j < 100; j++ {
                key := fmt.Sprintf("key-%d-%d", id, j)
                value := fmt.Sprintf("value-%d-%d", id, j)
                cache.Set(key, value, time.Hour)
                time.Sleep(time.Millisecond)
            }
        }(i)
    }
    
    // 启动 100 个读 Goroutine
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            for j := 0; j < 100; j++ {
                key := fmt.Sprintf("key-%d-%d", id%10, j)
                _, _ = cache.Get(key)
                time.Sleep(time.Millisecond)
            }
        }(i)
    }
    
    wg.Wait()
    fmt.Println("All goroutines completed")
    fmt.Printf("Cache size: %d\n", len(cache.items))
}

9.3 挑战练习:生产者-消费者模式

题目:使用 Mutex 和 Cond 实现一个生产者-消费者模式,支持多个生产者和多个消费者。

解题思路

  • 使用 Mutex 保护队列。
  • 使用 Cond 等待队列满或空的条件。
  • 实现生产者,在队列满时等待,在生产后通知消费者。
  • 实现消费者,在队列空时等待,在消费后通知生产者。
  • 测试多个生产者和多个消费者的并发场景。

常见误区

  • 没有在循环中检查条件,导致虚假唤醒。
  • 没有正确获取和释放锁,导致死锁。
  • 信号丢失,即先发送信号后等待。

分步提示

  1. 定义队列结构体,包含切片、Mutex 和 Cond。
  2. 实现 Enqueue 方法,在队列满时等待,在生产后通知消费者。
  3. 实现 Dequeue 方法,在队列空时等待,在消费后通知生产者。
  4. 启动多个生产者和多个消费者。
  5. 测试并发场景下的正确性。

参考代码

go
package main

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

type Queue struct {
    items []int
    mu    sync.Mutex
    cond  *sync.Cond
    size  int
}

func NewQueue(size int) *Queue {
    q := &Queue{
        items: make([]int, 0, size),
        size:  size,
    }
    q.cond = sync.NewCond(&q.mu)
    return q
}

func (q *Queue) Enqueue(item int) {
    q.mu.Lock()
    defer q.mu.Unlock()
    
    for len(q.items) >= q.size {
        fmt.Println("Queue full, waiting to enqueue")
        q.cond.Wait()
    }
    
    q.items = append(q.items, item)
    fmt.Printf("Enqueued: %d, queue size: %d\n", item, len(q.items))
    q.cond.Broadcast()
}

func (q *Queue) Dequeue() int {
    q.mu.Lock()
    defer q.mu.Unlock()
    
    for len(q.items) == 0 {
        fmt.Println("Queue empty, waiting to dequeue")
        q.cond.Wait()
    }
    
    item := q.items[0]
    q.items = q.items[1:]
    fmt.Printf("Dequeued: %d, queue size: %d\n", item, len(q.items))
    q.cond.Broadcast()
    
    return item
}

func main() {
    queue := NewQueue(5)
    var wg sync.WaitGroup
    
    // 启动 3 个生产者
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            for j := 0; j < 5; j++ {
                item := id*10 + j
                queue.Enqueue(item)
                time.Sleep(time.Millisecond * 100)
            }
        }(i)
    }
    
    // 启动 2 个消费者
    for i := 0; i < 2; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            for j := 0; j < 7; j++ {
                item := queue.Dequeue()
                fmt.Printf("Consumer %d: processed %d\n", id, item)
                time.Sleep(time.Millisecond * 150)
            }
        }(i)
    }
    
    wg.Wait()
    fmt.Println("All goroutines completed")
}

10. 知识点总结

10.1 核心要点

  • Mutex:互斥锁,确保同一时刻只有一个 Goroutine 可以访问共享资源。
  • 基本用法:使用 Lock() 加锁,Unlock() 解锁,或者使用 defer Unlock() 确保解锁。
  • 并发安全:Mutex 的所有方法都是并发安全的,使用原子操作来修改状态。
  • 零值可用:Mutex 的零值是可用的,不需要初始化。
  • 不可复制:Mutex 是结构体,不是引用类型,不要复制使用中的 Mutex。
  • 不可重入:同一个 Goroutine 不能多次获取同一个 Mutex,否则会导致死锁。
  • 性能优化:最小化锁的粒度,避免在持有锁时执行阻塞操作,使用双重检查锁定模式等。

10.2 易错点回顾

  • 死锁:多个 Goroutine 互相等待对方释放锁,导致程序无法继续执行。
  • 忘记解锁:在获取锁后,没有对应的解锁操作,导致其他 Goroutine 无法获取锁。
  • 锁粒度太大:长时间持有锁,执行了很多不需要锁保护的操作,导致并发性能下降。
  • 复制 Mutex:复制使用中的 Mutex,导致状态不同步。
  • 在持有锁时执行阻塞操作:增加锁的持有时间,降低并发性能。
  • 嵌套锁:在持有一个锁的同时获取另一个锁,增加死锁的风险。

11. 拓展参考资料

11.1 官方文档链接

11.2 进阶学习路径建议

  • 并发模式:学习常见的并发模式,如生产者-消费者、工作池、扇入扇出等。
  • 通道:深入学习通道的使用和通道模式,理解 CSP 并发模型。
  • Context:学习 Context 的使用,实现更复杂的并发控制,如超时控制、取消操作等。
  • 性能优化:学习并发性能优化技巧,如减少锁竞争、使用原子操作、优化缓存等。
  • 分布式系统:学习分布式系统中的并发控制,如分布式锁、一致性协议等。

11.3 相关资源