当前位置: 首页 > article >正文

Go 语言 sync 包使用教程

Go 语言 sync 包使用教程

Go 语言的 sync 包提供了基本的同步原语,用于在并发编程中协调 goroutine 之间的操作。

1. 互斥锁 (Mutex)

互斥锁用于保护共享资源,确保同一时间只有一个 goroutine 可以访问。

特点:

  • 最基本的同步原语,实现互斥访问共享资源
  • 有两个方法:Lock()Unlock()
  • 不可重入,同一个 goroutine 重复获取会导致死锁
  • 没有超时机制,锁定后必须等待解锁
  • 不区分读写操作,所有操作都是互斥的
  • 适用于共享资源竞争不激烈的场景
  • 性能高于 channel 实现的互斥机制
  • 不保证公平性,可能导致饥饿问题
import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var mutex sync.Mutex
    counter := 0
    
    for i := 0; i < 1000; i++ {
        go func() {
            mutex.Lock()
            defer mutex.Unlock()
            counter++
        }()
    }
    
    time.Sleep(time.Second)
    fmt.Println("计数器:", counter)
}

2. 读写锁 (RWMutex)

当多个 goroutine 需要读取而很少写入时,读写锁比互斥锁更高效。

特点:

  • 针对读多写少场景优化的锁
  • 提供四个方法:RLock()RUnlock()Lock()Unlock()
  • 允许多个读操作并发进行,但写操作是互斥的
  • 写锁定时,所有读操作都会被阻塞
  • 有读锁定时,写操作会等待所有读操作完成
  • 写操作优先级较高,防止写饥饿
  • 内部使用 Mutex 实现
  • 比 Mutex 有更多开销,但在读多写少场景下性能更高
var rwMutex sync.RWMutex
var data map[string]string = make(map[string]string)

// 读取操作
func read(key string) string {
    rwMutex.RLock()
    defer rwMutex.RUnlock()
    return data[key]
}

// 写入操作
func write(key, value string) {
    rwMutex.Lock()
    defer rwMutex.Unlock()
    data[key] = value
}

3. 等待组 (WaitGroup)

等待组用于等待一组 goroutine 完成执行。

特点:

  • 用于协调多个 goroutine 的完成
  • 提供三个方法:Add()Done()Wait()
  • Add() 增加计数器,参数可为负数
  • Done() 等同于 Add(-1),减少计数器
  • Wait() 阻塞直到计数器归零
  • 计数器不能变为负数,会导致 panic
  • 可以重用,计数器归零后可以再次增加
  • 非常适合"扇出"模式(启动多个工作 goroutine 并等待全部完成)
  • 不包含工作内容信息,仅表示完成状态
  • 轻量级,开销很小
func main() {
    var wg sync.WaitGroup
    
    for i := 0; i < 5; i++ {
        wg.Add(1) // 增加计数器
        go func(id int) {
            defer wg.Done() // 完成时减少计数器
            fmt.Printf("工作 %d 完成\n", id)
        }(i)
    }
    
    wg.Wait() // 等待所有 goroutine 完成
    fmt.Println("所有工作已完成")
}

4. 一次性执行 (Once)

Once 确保一个函数只执行一次,无论有多少 goroutine 尝试执行它。

特点:

  • 确保某个函数只执行一次
  • 只有一个方法:Do(func())
  • 即使在多个 goroutine 中调用也只执行一次
  • 常用于单例模式或一次性初始化
  • 内部使用互斥锁和一个标志位实现
  • 非常轻量级,几乎没有性能开销
  • 如果传入的函数 panic,视为已执行
  • 不能重置,一旦执行就不能再次执行
  • 传入不同的函数也不会再次执行
var once sync.Once
var instance *singleton

func getInstance() *singleton {
    once.Do(func() {
        instance = &singleton{}
    })
    return instance
}

5. 条件变量 (Cond)

条件变量用于等待或宣布事件的发生。

特点:

  • 用于等待或通知事件发生
  • 需要与互斥锁结合使用:sync.NewCond(&mutex)
  • 提供三个方法:Wait()Signal()Broadcast()
  • Wait() 自动解锁并阻塞,被唤醒后自动重新获取锁
  • Signal() 唤醒一个等待的 goroutine
  • Broadcast() 唤醒所有等待的 goroutine
  • 适合生产者-消费者模式
  • 可以避免轮询,提高性能
  • 使用相对复杂,容易出错
  • 等待必须在获取锁后调用
var mutex sync.Mutex
var cond = sync.NewCond(&mutex)
var ready bool

func main() {
    go producer()
    
    // 消费者
    mutex.Lock()
    for !ready {
        cond.Wait() // 等待条件变为真
    }
    fmt.Println("数据已准备好")
    mutex.Unlock()
}

func producer() {
    time.Sleep(time.Second) // 模拟工作
    
    mutex.Lock()
    ready = true
    cond.Signal() // 通知一个等待的 goroutine
    // 或使用 cond.Broadcast() 通知所有等待的 goroutine
    mutex.Unlock()
}

6. 原子操作 (atomic)

对于简单的计数器或标志,可以使用原子操作包而不是互斥锁。

特点:

  • 底层的原子操作,无锁实现
  • 适用于简单的计数器或标志位
  • 比互斥锁性能更高,开销更小
  • 提供多种原子操作:AddLoadStoreSwapCompareAndSwap
  • 支持多种数据类型:int32、int64、uint32、uint64、uintptr 和指针
  • 可用于实现自己的同步原语
  • 不适合复杂的共享状态
  • 在多 CPU 系统上可能导致缓存一致性开销
  • Go 1.19 引入了新的原子类型
import (
    "fmt"
    "sync/atomic"
    "time"
)

func main() {
    var counter int64 = 0
    
    for i := 0; i < 1000; i++ {
        go func() {
            atomic.AddInt64(&counter, 1)
        }()
    }
    
    time.Sleep(time.Second)
    fmt.Println("计数器:", atomic.LoadInt64(&counter))
}

7. Map (sync.Map)

Go 1.9 引入的线程安全的 map。

特点:

  • Go 1.9 引入的线程安全的哈希表
  • 无需额外加锁即可安全地并发读写
  • 提供五个方法:StoreLoadLoadOrStoreDeleteRange
  • 内部使用分段锁和原子操作优化性能
  • 适用于读多写少的场景
  • 不保证遍历的顺序
  • 不支持获取元素数量或判断是否为空
  • 不能像普通 map 那样直接使用下标语法
  • 性能比加锁的普通 map 更好,但单线程下比普通 map 慢
  • 内存开销较大
var m sync.Map

func main() {
    // 存储键值对
    m.Store("key1", "value1")
    m.Store("key2", "value2")
    
    // 获取值
    value, ok := m.Load("key1")
    if ok {
        fmt.Println("找到键:", value)
    }
    
    // 如果键不存在则存储
    m.LoadOrStore("key3", "value3")
    
    // 删除键
    m.Delete("key2")
    
    // 遍历所有键值对
    m.Range(func(key, value interface{}) bool {
        fmt.Println(key, ":", value)
        return true // 返回 false 停止遍历
    })
}

8. Pool (sync.Pool)

对象池用于重用临时对象,减少垃圾回收压力。

特点:

  • 用于缓存临时对象,减少垃圾回收压力
  • 提供两个方法:Get()Put()
  • 需要提供 New 函数来创建新对象
  • 对象可能在任何时候被垃圾回收,不保证存活
  • 在 GC 发生时会清空池中的所有对象
  • 不适合管理需要显式关闭的资源(如文件句柄)
  • 适合于频繁创建和销毁的对象
  • 没有大小限制,Put 总是成功的
  • 每个 P(处理器)有自己的本地池,减少竞争
  • Go 1.13 后大幅提升了性能
var bufferPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

func process() {
    // 获取缓冲区
    buffer := bufferPool.Get().(*bytes.Buffer)
    buffer.Reset() // 清空以便重用
    
    // 使用缓冲区
    buffer.WriteString("hello")
    
    // 操作完成后放回池中
    bufferPool.Put(buffer)
}

9. 综合示例

下面是一个综合示例,展示了多个同步原语的使用:

package main

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

type SafeCounter struct {
    mu sync.Mutex
    wg sync.WaitGroup
    count int
}

func main() {
    counter := SafeCounter{}
    
    // 启动 5 个 goroutine 增加计数器
    for i := 0; i < 5; i++ {
        counter.wg.Add(1)
        go func(id int) {
            defer counter.wg.Done()
            
            for j := 0; j < 10; j++ {
                counter.mu.Lock()
                counter.count++
                fmt.Printf("Goroutine %d: 计数器 = %d\n", id, counter.count)
                counter.mu.Unlock()
                
                // 模拟工作
                time.Sleep(100 * time.Millisecond)
            }
        }(i)
    }
    
    // 等待所有 goroutine 完成
    counter.wg.Wait()
    fmt.Println("最终计数:", counter.count)
}

最佳实践

  1. 使用 defer 解锁:确保即使发生错误也能解锁

    mu.Lock()
    defer mu.Unlock()
    
  2. 避免锁的嵌套:容易导致死锁

  3. 保持临界区简短:锁定时间越短越好

  4. 基准测试比较

    • 对于大多数简单操作,atomic 比 Mutex 快
    • RWMutex 在读操作远多于写操作时优于 Mutex
    • sync.Map 在高并发下比加锁的 map 性能更好
  5. 内存对齐

    • 原子操作需要内存对齐
    • 不正确的内存对齐会严重影响性能
    • 特别是在 32 位系统上使用 64 位原子操作
  6. 超时控制

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    done := make(chan struct{})
    go func() {
        // 执行可能耗时的操作
        mu.Lock()
        // ...
        mu.Unlock()
        
        done <- struct{}{}
    }()
    
    select {
    case <-done:
        // 操作成功完成
    case <-ctx.Done():
        // 操作超时
    }
    

http://www.kler.cn/a/611395.html

相关文章:

  • 内存型数据库深入解析:Memcache、Redis 与 Squid 的对比与应用
  • 数据结构二叉树进阶
  • SylixOS 中 select 原理及使用分析
  • 计算机三级信息安全技术核心知识点详细定义解析,按章节分类并重点阐述关键概念定义
  • 【加密社】如何创建自己的币圈工具站
  • 解决用户同时登录轮询获取用户信息错乱,使用WebSocket和Server-Sent Events (SSE)
  • 数据可视化TensorboardX和tensorBoard安装及使用
  • MySQL - 数据库基础操作
  • 【每日算法】Day 8-1:广度优先搜索(BFS)算法精讲——层序遍历与最短路径实战(C++实现)
  • 二十五、实战开发 uni-app x 项目(仿京东)- 前后端轮播图
  • 2025最新Chatbox全攻略:一键配置Claude/GPT/DeepSeek等主流模型(亲测可用)
  • # WebSocket 与 Socket.IO 对比与优化
  • RustDesk部署到linux(自建服务器)
  • How to use pgbench to test performance for PostgreSQL?
  • 完全背包模板
  • 突破反爬困境:SDK架构设计,为什么选择独立服务模式(四)
  • 本地部署 LangManus
  • K8S学习之基础五十一:k8s部署jenkins
  • 面试常问系列(二)-神经网络参数初始化之自注意力机制
  • 【hot100】刷题记录(52)-合并K个升序链表