Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > go标准库 sync

Go标准库sync功能实例详解

作者:女王大人万岁

Go语言通过goroutine实现轻量级并发,而sync库是并发编程中同步原语的核心集合,提供了多种用于协调goroutine执行、保护共享资源的工具,这篇文章给大家介绍Go标准库sync功能实例详解,感兴趣的朋友跟随小编一起看看吧

一、sync库核心定位与设计意义

Go语言通过goroutine实现轻量级并发,而sync库是并发编程中同步原语的核心集合,提供了多种用于协调goroutine执行、保护共享资源的工具。其核心价值在于解决并发场景下的资源竞争、执行顺序控制问题,是构建安全并发程序的基础。

1.1 核心定位与适用场景

1.2 channel与sync原语的适用场景划分

在Go并发编程中,channel与sync原语需根据业务场景按需选择,二者无绝对优劣,核心适配不同并发诉求:

优先使用channel的场景:一是goroutine间需要传递数据或消息,通过通信实现数据流转与解耦,避免共享内存带来的竞争问题;二是简单同步需求,如信号通知、goroutine间协作触发(如一方完成任务后告知另一方);三是流程编排场景,需通过管道串联多个goroutine的执行逻辑,实现上下游数据传递。

优先使用sync原语的场景:一是共享资源保护,需控制多个goroutine对同一资源的访问权限(如高频读写共享变量、并发修改字典),通过锁机制确保数据安全;二是精细同步控制,如等待一组goroutine全部完成(WaitGroup)、基于条件触发的goroutine唤醒(Cond)、确保函数仅执行一次(Once);三是临时对象复用(Pool),需减少内存分配与GC压力,提升高频场景性能。复杂并发场景中,二者可结合使用,兼顾数据安全与流程优雅性。

结论:简单同步用channel,高频共享资源保护、精细同步控制用sync原语,复杂场景可结合使用。

结论:简单同步用channel,高频共享资源保护、精细同步控制用sync原语,复杂场景可结合使用。

sync库提供的核心数据结构可分为四大类:互斥锁、同步控制、并发安全容器、辅助工具,以下逐一详解。

二、sync库主要功能

2.1 互斥锁:Mutex(排他锁)

功能:最基础的排他锁,确保同一时间只有一个goroutine能获取锁,访问共享资源,其他goroutine需阻塞等待锁释放。适用于读写频率相近或写操作频繁的场景。

核心原理:基于操作系统原子操作和信号量实现,内部通过状态标记控制锁的获取与释放,支持阻塞等待且不支持重入(同一goroutine不能重复获取已持有的锁)。Mutex包含两种工作模式,以平衡公平性与性能:

核心方法:

示例代码:

package main
import (
    "fmt"
    "sync"
    "time"
)
var (
    count int           // 共享资源
    mutex sync.Mutex    // 互斥锁保护count
)
// 对共享资源执行加1操作
func increment() {
    mutex.Lock()         // 加锁,排他访问
    defer mutex.Unlock() // 延迟释放锁,确保函数退出时必释放
    count++
    fmt.Printf("goroutine %d: count = %d\n", time.Now().UnixNano()%1000, count)
    time.Sleep(100 * time.Millisecond) // 模拟业务耗时
}
func main() {
    var wg sync.WaitGroup // 用于等待所有goroutine完成
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            increment()
        }()
    }
    wg.Wait() // 等待所有goroutine执行完毕
    fmt.Printf("最终count值:%d\n", count)
}

注意事项:

2.2 读写锁:RWMutex

功能:读写分离锁,将访问分为读操作和写操作,支持“多读单写”:同一时间可多个goroutine读,或一个goroutine写,读与写、写与写互斥。适用于读操作远多于写操作的场景(如缓存、配置文件读取)。

核心原理:内部维护读锁计数器和写锁状态,读锁获取时检查无写锁即可,写锁获取时需等待所有读锁和写锁释放,优先级通常为“写优先”(不同Go版本可能微调)。

核心方法:

示例代码:

package main
import (
    "fmt"
    "sync"
    "time"
)
var (
    data   = make(map[string]string) // 共享字典
    rwLock sync.RWMutex              // 读写锁保护data
)
// 读操作:获取数据,加读锁
func readData(key string) string {
    rwLock.RLock()
    defer rwLock.RUnlock()
    time.Sleep(50 * time.Millisecond) // 模拟读耗时
    return data[key]
}
// 写操作:更新数据,加写锁
func writeData(key, value string) {
    rwLock.Lock()
    defer rwLock.Unlock()
    time.Sleep(100 * time.Millisecond) // 模拟写耗时
    data[key] = value
    fmt.Printf("更新数据:%s=%s\n", key, value)
}
func main() {
    var wg sync.WaitGroup
    // 先初始化数据
    writeData("name", "Go")
    // 启动10个读goroutine
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(idx int) {
            defer wg.Done()
            val := readData("name")
            fmt.Printf("读goroutine %d: 读取到 %s\n", idx, val)
        }(i)
    }
    // 启动2个写goroutine
    for i := 0; i < 2; i++ {
        wg.Add(1)
        go func(idx int) {
            defer wg.Done()
            writeData("name", fmt.Sprintf("Go-%d", idx))
        }(i)
    }
    wg.Wait()
    fmt.Println("最终数据:", data["name"])
}

注意事项:

2.3 等待组:WaitGroup

功能:用于等待一组goroutine全部执行完成,主goroutine阻塞直至所有子goroutine调用Done(),适用于批量任务同步(如并发处理任务后汇总结果)。

核心原理:内部维护一个计数器,Add(n)增加计数,Done()减少计数,Wait()阻塞直至计数为0。计数器为负时会引发panic。

核心方法:

示例代码:

package main
import (
    "fmt"
    "sync"
    "time"
)
// 模拟任务处理
func processTask(id int, wg *sync.WaitGroup) {
    defer wg.Done() // 任务完成后计数器减1
    fmt.Printf("任务 %d 开始执行\n", id)
    time.Sleep(time.Duration(id*100) * time.Millisecond) // 模拟任务耗时
    fmt.Printf("任务 %d 执行完成\n", id)
}
func main() {
    var wg sync.WaitGroup
    taskCount := 5
    // 新增5个任务,计数器设为5
    wg.Add(taskCount)
    for i := 0; i < taskCount; i++ {
        go processTask(i, &wg) // 传递WaitGroup指针,避免值拷贝
    }
    fmt.Println("等待所有任务完成...")
    wg.Wait() // 阻塞直至所有任务完成
    fmt.Println("所有任务均已完成,程序退出")
}

注意事项:

2.4 条件变量:Cond

功能:基于互斥锁实现的goroutine通知机制,支持“等待-通知”模式:多个goroutine等待某个条件满足,当条件满足时,由一个或多个goroutine发送通知唤醒等待者。适用于复杂同步场景(如生产者-消费者模型)。

核心原理:与Mutex/RWMutex绑定,等待者需先持有锁,然后调用Wait()释放锁并阻塞;通知者发送通知后,等待者重新获取锁并继续执行。

核心方法:

示例代码(生产者-消费者模型):

package main
import (
    "fmt"
    "sync"
    "time"
)
var (
    queue  []int         // 消息队列
    mutex  sync.Mutex    // 保护队列
    cond   = sync.NewCond(&mutex) // 绑定互斥锁的条件变量
    maxLen = 5           // 队列最大长度
)
// 生产者:向队列添加数据
func producer(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 0; i < 3; i++ {
        mutex.Lock()
        // 队列满时,等待消费者消费(条件不满足)
        for len(queue) >= maxLen {
            cond.Wait() // 释放锁,阻塞等待通知
        }
        // 生产数据
        data := id*10 + i
        queue = append(queue, data)
        fmt.Printf("生产者 %d: 生产数据 %d,队列长度:%d\n", id, data, len(queue))
        mutex.Unlock()
        cond.Signal() // 唤醒一个消费者
        time.Sleep(300 * time.Millisecond)
    }
}
// 消费者:从队列获取数据
func consumer(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 0; i < 2; i++ {
        mutex.Lock()
        // 队列空时,等待生产者生产(条件不满足)
        for len(queue) == 0 {
            cond.Wait() // 释放锁,阻塞等待通知
        }
        // 消费数据
        data := queue[0]
        queue = queue[1:]
        fmt.Printf("消费者 %d: 消费数据 %d,队列长度:%d\n", id, data, len(queue))
        mutex.Unlock()
        cond.Signal() // 唤醒一个生产者
        time.Sleep(500 * time.Millisecond)
    }
}
func main() {
    var wg sync.WaitGroup
    // 启动2个生产者,3个消费者
    wg.Add(2 + 3)
    for i := 0; i < 2; i++ {
        go producer(i, &wg)
    }
    for i := 0; i < 3; i++ {
        go consumer(i, &wg)
    }
    wg.Wait()
    fmt.Println("生产消费完成,队列剩余数据:", queue)
}

注意事项:

2.5 单次执行:Once

功能:确保某个函数在整个程序生命周期内只执行一次,无论多少goroutine调用,适用于单例初始化、资源一次性加载等场景。

核心原理:内部通过原子操作标记函数是否已执行,首次调用Do(f)时执行函数f,后续调用直接返回,不执行f。

核心方法:

示例代码(单例初始化):

package main
import (
    "fmt"
    "sync"
    "time"
)
type Config struct {
    Host string
    Port int
}
var (
    config *Config
    once   sync.Once
)
// 加载配置,确保只执行一次
func loadConfig() *Config {
    fmt.Println("开始加载配置(仅执行一次)")
    // 模拟配置加载耗时(如读取文件、远程请求)
    time.Sleep(1 * time.Second)
    return &Config{
        Host: "localhost",
        Port: 8080,
    }
}
// 获取配置单例
func GetConfig() *Config {
    once.Do(func() {
        config = loadConfig()
    })
    return config
}
func main() {
    var wg sync.WaitGroup
    // 10个goroutine同时获取配置
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(idx int) {
            defer wg.Done()
            cfg := GetConfig()
            fmt.Printf("goroutine %d: 获取配置 %+v\n", idx, cfg)
        }(i)
    }
    wg.Wait()
}

注意事项:

2.6 临时对象池:Pool

功能:专为高频创建、可复用的临时对象设计,核心价值是通过对象复用减少内存分配次数,降低GC压力,适用于算法实现、高频计算、序列化/反序列化等底层场景。其设计定位决定了它不适合业务层使用,尤其不能用于存储需要持久化的业务数据(如会话、配置、订单信息),仅适用于无状态、可随时丢弃的临时对象。

核心原理:内部采用“本地缓存+全局缓存”的分层结构,每个P(逻辑处理器)对应一个本地缓存,减少跨P竞争:

核心方法:

示例代码(字节缓冲区复用):

package main
import (
    "bytes"
    "fmt"
    "sync"
    "time"
)
// 创建字节缓冲区对象池
var bufPool = sync.Pool{
    New: func() interface{} {
        fmt.Println("创建新的缓冲区(无复用对象时)")
        return &bytes.Buffer{}
    },
}
// 处理数据:复用缓冲区
func processData(data string) {
    // 从池中获取缓冲区
    buf := bufPool.Get().(*bytes.Buffer)
    defer func() {
        buf.Reset()       // 清空缓冲区,确保下次复用干净
        bufPool.Put(buf)  // 归还缓冲区到池
    }()
    // 模拟数据处理
    buf.WriteString("处理数据:")
    buf.WriteString(data)
    fmt.Println(buf.String())
    time.Sleep(100 * time.Millisecond)
}
func main() {
    var wg sync.WaitGroup
    // 20个goroutine复用缓冲区
    for i := 0; i < 20; i++ {
        wg.Add(1)
        go func(idx int) {
            defer wg.Done()
            processData(fmt.Sprintf("message-%d", idx))
        }(i)
    }
    wg.Wait()
}

注意事项:

2.7 并发安全字典:Map

功能:并发安全的键值对容器,专为读多写少场景设计,通过读写分离机制优化并发性能,替代“原生map+锁”的方案。其核心优势在于读操作无锁(依赖只读快照),写操作加全局互斥锁,适合缓存、配置存储等读请求占比极高的场景;若写操作频繁(写占比超过20%),则“原生map+Mutex/RWMutex”可能更高效,避免 sync.Map 双字典切换及锁竞争带来的开销。

核心原理:基于“读写分离+原子操作+全局互斥锁”实现,内部维护 readOnly 和 dirty 两个字典,无分段锁(shard)结构,从 Go 1.9 引入至今(含 1.25.3 版本)核心实现逻辑一致,兼顾读性能与写安全性:

核心方法:

示例代码:

package main
import (
    "fmt"
    "sync"
    "time"
)
func main() {
    var m sync.Map
    var wg sync.WaitGroup
    // 启动5个写goroutine
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(idx int) {
            defer wg.Done()
            key := fmt.Sprintf("key-%d", idx)
            value := idx * 10
            m.Store(key, value)
            fmt.Printf("写goroutine %d: 存储 %s=%d\n", idx, key, value)
            time.Sleep(50 * time.Millisecond)
        }(i)
    }
    wg.Wait()
    // 遍历字典
    fmt.Println("\n遍历字典:")
    m.Range(func(key, value interface{}) bool {
        fmt.Printf("%v=%v\n", key, value)
        return true // 继续遍历
    })
    // 读取指定键
    key := "key-2"
    val, ok := m.Load(key)
    if ok {
        fmt.Printf("\n读取 %s: %v\n", key, val)
    }
    // 删除指定键
    m.Delete(key)
    val, ok = m.Load(key)
    fmt.Printf("删除 %s 后,是否存在:%v,值:%v\n", key, ok, val)
}

注意事项:

三、sync库实战案例(综合场景)

3.1 案例:并发任务调度与结果汇总

结合WaitGroup、Mutex,实现并发执行多个任务,汇总所有任务结果,确保结果安全存储。

package main
import (
    "fmt"
    "sync"
    "time"
)
// 任务函数:生成结果
func task(id int) int {
    time.Sleep(time.Duration(id*200) * time.Millisecond)
    return id * 10 // 模拟任务结果
}
func main() {
    var wg sync.WaitGroup
    var mutex sync.Mutex
    taskCount := 6
    results := make([]int, 0, taskCount) // 存储任务结果
    // 并发执行任务
    wg.Add(taskCount)
    for i := 0; i < taskCount; i++ {
        go func(idx int) {
            defer wg.Done()
            res := task(idx)
            // 安全写入结果(加锁保护切片)
            mutex.Lock()
            results = append(results, res)
            mutex.Unlock()
            fmt.Printf("任务 %d 完成,结果:%d\n", idx, res)
        }(i)
    }
    wg.Wait()
    fmt.Println("所有任务完成,结果汇总:", results)
}

四、sync库使用避坑指南

并发编程中,sync原语的不当使用易导致死锁、数据竞争、性能损耗等问题,以下是高频坑点及解决方案:

坑点类型具体问题错误现象解决方案
死锁1. 同一goroutine重复获取Mutex;2. 多个goroutine交叉持有锁程序阻塞,无法继续执行1. 避免重入锁;2. 统一锁获取顺序;3. 用TryLock()非阻塞获取
数据竞争共享资源未加锁保护,或锁范围不当数据错乱、程序崩溃1. 明确共享资源,全程加锁保护;2. 缩小锁范围(仅包裹临界区)
性能损耗1. 读多写少场景用Mutex;2. 锁范围过大;3. 写频繁场景用sync.Map并发性能低下,CPU利用率低1. 读多写少用RWMutex;2. 缩小锁范围;3. 写频繁用“原生map+RWMutex”
资源泄露1. WaitGroup未调用Done();2. Cond等待后未唤醒程序阻塞、goroutine泄露1. 用defer确保Done()调用;2. 成对使用Cond的等待与通知
对象池误用用Pool存储持久化数据数据丢失(被GC回收)Pool仅用于临时对象,持久化数据用全局变量或数据库

五、总结与选型建议

5.1 核心总结

sync库是Go并发编程的基础工具集,提供了从简单锁机制到复杂同步控制、并发容器的完整能力。其核心是通过合理的同步原语,平衡“并发效率”与“数据安全”,避免资源竞争和执行顺序混乱。

5.2 选型建议

最终,sync原语的使用需结合业务场景和并发模型,优先保证数据安全,再优化并发性能,必要时可结合channel实现更优雅的并发控制。

到此这篇关于Go标准库sync功能实例详解的文章就介绍到这了,更多相关go标准库 sync 内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文