Go语言中的**
sync包提供了并发编程所需的基本同步原语,例如互斥锁、条件变量、等待组等, 主要解决并发编程中goroutine之间的数据竞争和协调问题**、协调多个goroutine等常见问题。以下是核心组件详解及使用场景:
一.锁实现原理与解析
1.Mutex 底层结构
Go
type Mutex struct {
state int32 // 复合状态字段(包含锁状态、等待者数量等)
sema uint32 // 信号量,用于阻塞/唤醒协程
}
state 字段的二进制布局:
-
第0位:
locked(是否已锁:0未锁,1已锁) -
第1位:
woken(是否有协程被唤醒) -
第2位:
starving(是否饥饿模式) -
剩余位:
waiter(等待锁的协程数量)
2.锁的几种模式
-
正常模式:
-
新来的协程会尝试**自旋(约4次)抢锁,**若失败则进入等待队列
-
唤醒等待者时需与新来的协程竞争,可能导致等待者长期饥饿
-
-
饥饿模式(当等待时间 > 1ms 时触发):
-
新来的协程直接排队,不尝试抢锁
-
锁直接交给等待队列最前端的协程
-
当等待者执行完毕且无其他等待者时,切换回正常模式
-
3.核心方法解析
(1).Lock() 流程
Go
func (m *Mutex) Lock() {
// 快速路径:直接获取未锁定的mutex
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
return
}
m.lockSlow() // 进入慢路径
}
在lockSlow()中:
-
若当前是正常模式且能自旋,则循环尝试CAS抢锁。
-
自旋失败后累加等待计数,尝试设置锁状态。
-
若等待超时,标记为饥饿模式
(2).Unlock() 流程
Go
func (m *Mutex) Unlock() {
// 快速解锁(无竞争时)
new := atomic.AddInt32(&m.state, -mutexLocked)
if new != 0 {
m.unlockSlow(new) // 有等待者需处理
}
}
在unlockSlow()中:
-
若存在等待者,根据模式决定唤醒策略:
-
饥饿模式:直接移交锁给下一个等待者。
-
正常模式:唤醒一个等待者并允许其与新来协程竞争。
-
4.场景分析
(1).基础用法(银行转账)
Go
type Account struct {
balance int
mu sync.Mutex
}
func (a *Account) Transfer(to *Account, amount int) {
a.mu.Lock()
defer a.mu.Unlock()
to.mu.Lock()
defer to.mu.Unlock()
a.balance -= amount
to.balance += amount
}
风险:嵌套锁可能导致死锁(如A转B同时B转A)。
改进 :使用**
sync.RWMutex**或调整锁定顺序
(2).饥饿模式演
Go
func main() {
var mu sync.Mutex
done := make(chan bool)
// 长期持有锁的协程
go func() {
mu.Lock()
time.Sleep(2 * time.Second) // 模拟耗时操作
mu.Unlock()
done <- true
}()
// 后续协程因饥饿进入等待队列
go func() {
time.Sleep(500 * time.Millisecond)
mu.Lock() // 此处将触发饥饿模式
mu.Unlock()
done <- true
}()
<-done; <-done
}
(3).读写分离
Go
var cache struct {
data map[string]string
rw sync.RWMutex // 读多写少场景
}
func Get(key string) string {
cache.rw.RLock()
defer cache.rw.RUnlock()
return cache.data[key]
}
(4).避免复制锁
Go
type Container struct {
mu sync.Mutex
// ...
}
func main() {
c := Container{}
go func(copy Container) { // 错误!复制了锁
copy.mu.Lock() // 导致不可预测行为
}(c)
}
5.底层机制关键点
-
自旋优化:在正常模式下,新协程通过有限自旋(CPU空转)尝试避免进入内核阻塞,减少上下文切换开销
-
信号量操作 :底层依赖
runtime.semacquire()和runtime.semrelease(),利用FIFO队列管理等待协程 -
内存屏障 :通过
atomic操作保证锁状态变更的可见性,符合Go内存模型要求
sync.Mutex通过复合状态位+信号量+模式切换实现了高效公平的锁机制:
正常模式:优先自旋,兼顾吞吐量
饥饿模式:防止长等待协程饿死
原子操作:确保状态变更的原子性
实际开发中应结合场景选择同步原语(如
RWMutex、sync.Map或atomic),并通过go test -race检测竞态条件
二.几种常见的锁及其原理
1. Mutex(互斥锁)
功能:互斥锁用于保护共享资源,确保同一时刻只有一个goroutine可以访问该资源, 防止多个goroutine同时访问共享资源。
它有两种状态:锁定(locked)和未锁定(unlocked)。
当锁已被持有时,其他尝试获取锁的goroutine会被阻塞,直到锁被释放。
底层通过原子操作和信号量(sema)实现,结合了自旋和休眠等待机制,以提高性能
方法:
Lock(): 获取锁,如果锁已被占用,则调用者会被阻塞直到锁可用
Unlock(): 释放锁
底层结构
Go
type Mutex struct {
state int32 // 复合状态(锁标志/饥饿模式/等待计数)
sema uint32 // 信号量
}
-
工作模式:
-
正常模式:新协程尝试自旋(约4次)抢锁,失败后入队
-
饥饿模式(等待超1ms触发):新协程直接入队,锁按FIFO顺序移交
-
-
原子操作 :通过
atomic.CompareAndSwapInt32实现无锁状态变更
账户安全转账
Gotype BankAccount struct { balance int mu sync.Mutex } func (acc *BankAccount) Transfer(to *BankAccount, amount int) { acc.mu.Lock() defer acc.mu.Unlock() to.mu.Lock() defer to.mu.Unlock() acc.balance -= amount to.balance += amount }
使用场景:保护共享数据的读写操作(如计数器、配置信息), 当有多个goroutine需要读写同一个共享变量或数据结构时,使用互斥锁来避免数据竞争
示例:多个goroutine并发更新一个计数器
package main
import (
"fmt"
"sync"
)
type Counter struct {
mu sync.Mutex
count int
}
func (c *Counter) Increment() {
c.mu.Lock() // 加锁
defer c.mu.Unlock() // 函数结束时解锁
c.count++
}
func main() {
counter := Counter{}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}
wg.Wait()
fmt.Println(counter.count) // 输出1000(无竞争)
}
Go
// 从Prometheus中获取对应的 数据
func getSlotsPrometheusGamePool0(GameIdBet []*backstage.GameIdBet) ([]*backstage.SlotsPoolIndexCurModel, error) {
curlPrometheus := server.FM.RemoteConfig.Setting["gameonline"]
//curlPrometheus = "192.168.1.1"
if curlPrometheus == "" {
return nil, fmt.Errorf("Prometheus服务地址未配置")
}
// 准备通过协程并发查询所需数据
var (
results []*backstage.SlotsPoolIndexCurModel // 最终结果集
wg sync.WaitGroup // 等待组控制协程
mu sync.Mutex // 互斥锁保证线程安全
errChan = make(chan error, len(GameIdBet)) // 错误通道
)
// 遍历所有ID和档位组合
for _, gb := range GameIdBet {
wg.Add(1) // 每个任务+1
// 启动协程并发查询
go func(gameId int32) {
defer wg.Done() // 确保协程结束时通知等待组
val, err := GetServiceGamePoolValue(curlPrometheus, gameId)
if err != nil {
errChan <- fmt.Errorf("机台ID:%d 查询失败: %v", gameId, err)
return
}
// 线程安全地添加结果
mu.Lock()
results = append(results, &backstage.SlotsPoolIndexCurModel{
GameId: uint64(gameId),
PoolValue: val,
})
mu.Unlock()
}(gb.GameId)
}
// 等待所有协程完成
wg.Wait()
close(errChan) // 关闭错误通道
// 检查是否有错误发生
for err := range errChan {
if err != nil {
return results, fmt.Errorf("查询过程中发生错误: %v", err)
}
}
// 返回最终结果
return results, nil
}
2. RWMutex(读写锁)
功能:读写锁允许多个读操作同时进行,但写操作时完全互斥(写时不能读,也不能同时有多个写)
当有写锁时,所有读锁和写锁都会被阻塞。
当有读锁时,写锁会被阻塞,但读锁可以继续获取。
底层通过两个互斥锁和一个计数器实现:一个互斥锁用于写操作,另一个用于读操作,计数器记录当前读操作的数量
方法:
Lock()和Unlock():写锁
RLock()和RUnlock():读锁
底层结构
Go
type RWMutex struct {
w Mutex // 写锁
writerSem uint32 // 写等待信号量
readerSem uint32 // 读等待信号量
readerCount int32 // 当前读协程数
readerWait int32 // 等待读完成数
}
-
读锁:多个协程可同时获取
-
写锁:独占锁,获取时阻塞所有读写操作
-
优先级:写锁优先(防止读锁饿死写锁)
使用场景:读多写少的场景(如缓存系统)。
Go
type Config struct {
data map[string]string
rw sync.RWMutex // 读写锁
}
// 高频读操作
func (c *Config) Get(key string) string {
c.rw.RLock() // 读锁定(允许多个读)
defer c.rw.RUnlock()
return c.data[key]
}
// 低频写操作
func (c *Config) Set(key, value string) {
c.rw.Lock() // 写锁定(独占)
defer c.rw.Unlock()
c.data[key] = value
}
func main() {
cfg := Config{data: make(map[string]string)}
// 启动多个读goroutine
for i := 0; i < 10; i++ {
go func() {
for {
val := cfg.Get("key")
fmt.Println("Read:", val)
time.Sleep(10 * time.Millisecond)
}
}()
}
// 启动一个写goroutine
go func() {
count := 0
for {
cfg.Set("key", fmt.Sprintf("value%d", count))
count++
time.Sleep(100 * time.Millisecond)
}
}()
time.Sleep(time.Second)
}
Go
package main
import (
"fmt"
"sync"
"time"
)
var (
data map[string]string
rwLock sync.RWMutex
)
func readData(key string) string {
rwLock.RLock()
defer rwLock.RUnlock()
return data[key]
}
func writeData(key, value string) {
rwLock.Lock()
defer rwLock.Unlock()
data[key] = value
}
func main() {
data = make(map[string]string)
// 写数据
go func() {
for i := 0; i < 5; i++ {
writeData(fmt.Sprintf("key%d", i), fmt.Sprintf("value%d", i))
time.Sleep(100 * time.Millisecond)
}
}()
// 读数据
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 5; j++ {
val := readData(fmt.Sprintf("key%d", j))
fmt.Printf("Goroutine %d: key%d -> %s\n", id, j, val)
time.Sleep(50 * time.Millisecond)
}
}(i)
}
wg.Wait()
}
Go
// 监控服务
type MonitorWarnService struct {
db *gorm.DB
mu sync.RWMutex
monitors map[string]*MonitorWarn // key: "gameId_Id"
alertChan chan *AlertWarnMessage
ctx context.Context
cancel context.CancelFunc
checkInterval time.Duration
// 添加飞书机器人配置
feishuRobotConfig *appConfig.FeishuRobotConfig
}
//告警监控器
type MonitorWarn struct {
Id uint32 // 配置Id
GameId uint32 // 玩法id
Id uint32 // 房间id
GameName string // 玩法名称
WarnValue int64 // 告警阈值
WarnTime uint32 // 告警频率
LastAlert time.Time // 上次告警时间
AlertCount int // 告警次数
// 加载状态为1(进行中)的监控配置
func (m *MonitorWarnService) loadActiveMonitors() error {
var warnConfigs []model.WarnConfig
if err := m.db.Where("status = ?", uint32(backstage.WarnStatus_WARN_STATUS_NORMAL)).Find(&warnConfigs).Error; err != nil {
return err
}
m.mu.Lock()
defer m.mu.Unlock()
// 清空现有监控器
m.monitors = make(map[string]*MonitorWarn)
// 添加新的监控配置
for _, config := range warnConfigs {
key := m.getMonitorKey(config.GameId, config.BetId)
m.monitors[key] = &MonitorFishWarn{
Id: config.Id,
GameId: config.GameId,
Id: config.Id,
GameName: config.GameName,
WarnValue: config.WarnValue,
WarnTime: config.WarnTime,
}
mylog.Infof("加载告警监控配置: 配置Id: %d, 玩法: %s, GameId: %d, BetId: %d, 告警阈值: %d, 告警频率: %d分钟/次", config.Id, config.GameName, config.GameId, config.Id, config.WarnValue, config.WarnTime)
}
return nil
}
3. WaitGroup(等待组)
功能:用于等待一组goroutine完成。主goroutine通过Add设置等待的goroutine数量,每个goroutine完成后调用Done,Wait会阻塞直到所有goroutine完成。
方法:
Add(delta int): 增加等待的goroutine数量,delta可为负数(但通常为正)。
Done(): 等同于Add(-1)。
Wait(): 阻塞直到计数器归零使用场景:主goroutine需要等待所有子任务结束。
func main() {
var wg sync.WaitGroup
urls := []string{"url1", "url2", "url3"}
for _, url := range urls {
wg.Add(1) // 增加计数器
go func(u string) {
defer wg.Done() // 任务完成时计数器减1
fmt.Println("Fetching:", u)
}(url)
}
wg.Wait() // 阻塞直到计数器归零
fmt.Println("All tasks done")
}
4. Once(单次执行)
功能:确保某个操作在并发环境中只执行一次(如初始化),无论有多少个goroutine调用,无论有多少个goroutine调用且所有调用Once.Do的goroutine都会等待该操作执行完成。
- 内部使用一个互斥锁和一个布尔标志,当第一次调用
Do方法时,执行传入的函数并将标志置为已执行,后续调用将不再执行
Go
type Once struct {
done uint32 // 完成标志
m Mutex // 互斥锁
}
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 0 {
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
-
双检查机制:原子操作+互斥锁保证函数只执行一次
-
内存屏障 :
atomic.StoreUint32确保写入可见性
使用场景:单例模式或延迟初始化。
Go
package main
import (
"fmt"
"sync"
)
type Singleton struct {
// 假设这里有一些字段
}
var (
instance *Singleton
once sync.Once
)
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{} // 仅执行一次
})
return instance
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
ins := GetInstance()
fmt.Printf("%p\n", ins) // 打印的地址都相同
wg.Done()
}()
}
wg.Wait()
}
线程安全的单例初始化
Go
var (
instance *Service
once sync.Once
)
func GetService() *Service {
once.Do(func() {
instance = &Service{endpoint: "https://api.example.com"}
})
return instance
}
Go
package main
import (
"fmt"
"sync"
)
var once sync.Once
func setup() {
fmt.Println("Initialization done.")
}
func main() {
for i := 0; i < 5; i++ {
go func() {
once.Do(setup)
}()
}
// 等待goroutine执行
time.Sleep(1 * time.Second)
}
// 输出: Initialization done. (只输出一次)
Go
var (
globalMonitor *MonitorWarnService
globalMonitorOnce sync.Once
globalMonitorMu sync.RWMutex
)
// 启动Slots告警监控服务
func InitMonitorWarnService(db *gorm.DB) {
// 判断是否启用飞书告警服务
service := &MonitorWarnService{}
robotConfig, err := service.getWarnRobotConfig()
if err != nil {
panic(fmt.Sprintf("获取飞书机器人配置失败: %v", err))
}
// 检查机器人是否启用
if !robotConfig.IsEnable {
mylog.Infof("Slots游戏水池告警的飞书机器人未启用, 跳过发送告警")
return
}
globalMonitorOnce.Do(func() {
// 创建监控服务
monitorService := NewMonitorWarnService(db, time.Duration(robotConfig.CheckInterval)*time.Second, robotConfig)
// 启动监控服务
if err := monitorService.Start(); err != nil {
panic(fmt.Sprintf("Slots告警监控服务启动失败: %v", err))
}
// 设置全局实例
SetMonitorSlotsWarn(monitorService)
mylog.Infof("Slots告警监控系统已启动")
// 在单独的goroutine中等待中断信号,不阻塞主程序
go func() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
mylog.Infof("收到停止信号,正在关闭监控服务...")
monitorService.Stop()
mylog.Infof("监控服务已正常退出")
// 退出程序
os.Exit(0)
}()
})
}
Go
wg := sync.WaitGroup{}
wg.Add(9)
go func() {
defer wg.Done()
// 逻辑代码
}()
wg.Wait()
5. Cond(条件变量)
功能:条件变量用于在多个goroutine之间进行通知,常与互斥锁配合使用。当某个条件不满足时,goroutine会进入等待状态,直到另一个goroutine修改条件并发出通知。
创建 :
sync.NewCond(l Locker),其中l通常是一个Mutex或RWMutex。方法:
Wait(): 释放锁并挂起当前goroutine,等待通知。被唤醒后会重新获取锁。
Signal(): 唤醒一个等待的goroutine。
Broadcast(): 唤醒所有等待的goroutine。使用模式:需配合互斥锁和条件检查
Go
type Cond struct {
L Lock // 关联的锁(通常是Mutex)
// 其他私有字段
}
使用场景:生产者-消费者模型。
Go
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var (
m sync.Mutex
cond = sync.NewCond(&m)
queue []int
)
// 消费者
for i := 0; i < 2; i++ {
go func(id int) {
for {
m.Lock()
for len(queue) == 0 {
cond.Wait() // 等待队列不为空
}
item := queue[0]
queue = queue[1:]
fmt.Printf("Consumer %d got %d\n", id, item)
m.Unlock()
}
}(i)
}
// 生产者
go func() {
for i := 0; ; i++ {
m.Lock()
queue = append(queue, i)
cond.Signal() // 通知一个消费者
m.Unlock()
time.Sleep(100 * time.Millisecond)
}
}()
time.Sleep(time.Second)
}
Go
type TaskQueue struct {
tasks []Task
cond *sync.Cond
}
func NewQueue() *TaskQueue {
q := &TaskQueue{}
q.cond = sync.NewCond(&sync.Mutex{})
return q
}
// 生产者
func (q *TaskQueue) Add(task Task) {
q.cond.L.Lock()
defer q.cond.L.Unlock()
q.tasks = append(q.tasks, task)
q.cond.Signal() // 唤醒一个消费者
}
// 消费者
func (q *TaskQueue) Pop() Task {
q.cond.L.Lock()
defer q.cond.L.Unlock()
for len(q.tasks) == 0 {
q.cond.Wait() // 释放锁并等待
}
task := q.tasks[0]
q.tasks = q.tasks[1:]
return task
}
6. Pool(对象池)
功能:用于存储和复用临时对象,减少内存分配和GC压力
**注意:**Pool中的对象可能会随时被回收。
方法:
Get() interface{}: 从池中取出一个对象,如果池为空则调用New函数(如果设置了New字段)创建一个。
Put(x interface{}): 将对象放回池中使用场景:高频创建/销毁临时对象的场景(如网络连接池,缓冲区(bytes.Buffer))。
Go
var bufPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer) // 创建新对象
},
}
func Process(data string) {
buf := bufPool.Get().(*bytes.Buffer) // 从池中获取
defer bufPool.Put(buf) // 放回池中
buf.Reset()
buf.WriteString(data)
fmt.Println(buf.String())
}
func main() {
Process('hello')
}
7. Map(并发安全映射)
功能 :并发安全的map,适用于读多写少或者多个goroutine操作的键集合不相交(即每个键只被一个goroutine写)的情况。比使用Mutex+Map的组合在特定场景下性能更好。
内部通过读写分离和原子操作实现,避免使用锁导致性能下降。
它通过两个映射(一个只读,一个可写)和原子操作来保证并发安全。
方法:
Load(key interface{}) (value interface{}, ok bool)
Store(key, value interface{})
Delete(key interface{})
LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)
Range(f func(key, value interface{}) bool)使用场景:高频读/低频写的键值存储。
示例:多个goroutine并发读写不同的键
var safeMap sync.Map
func main() {
safeMap.Store("key1", 100) // 写操作
value, _ := safeMap.Load("key1") // 读操作
fmt.Println(value) // 输出100
safeMap.Range(func(k, v interface{}) bool {
fmt.Println(k, v) // 遍历所有键值
return true
})
}
8.atomic(原子操作)
功能 :
sync/atomic包提供了原子操作,用于对基本类型(int32, int64, uint32, uint64, uintptr)进行原子读写。
原子操作通过使用CPU原子指令实现,无需加锁,性能更高
AddT:原子加减
CompareAndSwapT:CAS操作
LoadT/StoreT:原子读写适用于简单的计数器等场景
无锁编程:适用于简单状态更新,避免锁开销
Go
package main
import (
"fmt"
"sync"
"sync/atomic"
)
var counter int64
func increment() {
atomic.AddInt64(&counter, 1)
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait()
fmt.Println("Counter:", counter) // 输出: Counter: 1000
}
无锁计数器
Go
type Counter struct {
value int64
}
func (c *Counter) Increment() {
atomic.AddInt64(&c.value, 1)
}
func (c *Counter) Value() int64 {
return atomic.LoadInt64(&c.value)
}
9.errgroup.Group
带错误传播的协程组
Go
var g errgroup.Group
g.Go(func() error { /* 任务1 */ })
g.Go(func() error { /* 任务2 */ })
if err := g.Wait(); err != nil {
// 处理首个错误
}
总结
| 组件 | 核心用途 | 典型场景 |
|---|---|---|
Mutex |
互斥访问共享资源 | 计数器、配置更新 |
RWMutex |
读写分离锁 | 读多写少的缓存 |
WaitGroup |
等待一组任务完成 | 批量任务并行处理 |
Once |
确保单次执行 | 单例初始化 |
Cond |
条件等待和唤醒 | 生产者-消费者模型 |
Pool |
对象复用减少GC开销 | 网络连接池、缓冲区管理 |
Map |
线程安全的键值存储 | 高频读取的全局配置 |
正确使用sync包可以避免数据竞争和死锁问题,确保并发程序的安全性和性能。
