每日一Go-40、深入-无锁编程-- atomic 和内存屏障的真实场景应用

0 阅读3分钟

一、为什么要无锁

1.1 Mutex的真实成本

  • 系统调用/竞争,高并发下可能进入内核态

  • 调度干扰,阻塞会触发G/M/P迁移

  • 可伸缩性差,核数越多,锁竞争越严重

1.2 无锁适合的典型场景

  • 高频读、极低写(统计、计数)

  • 状态机/标志位

  • 读多写少的配置快照

  • 自旋极短的临界区

二、atomic解决的核心问题

atomic本质解决三件事:

  • 2.1 原子性:操作不可被打断

  • 2.2 可见性:一个goroutine的写,其他goroutine能看到

  • 2.3 有序性:禁止CPU/编译器乱序

但是,atomic不解决复杂不变量,那是锁的工作。

三、atomic的能力边界

3.1 atomic能做什么

  • 原子读写:Load/Store

  • 原子更新:Add/Swap/CompareAndSwap

  • 作为内存屏障(Memory Barrier)

3.2 atomic不能做什么

  • 不能保护多个变量的一致性

  • 不能表达复杂临界区

  • 不能替代mutex的条件同步

四、atomic的真实语义:不是“变量”,是“屏障”

4.1 Go内存模型中的关键规则:对同一个变量的atomic Store happens-before 之后的atomic Load,这意味着:

atomic = 发布(release—)+获取(acquire)语义

4.2 最重要的一条原则:

不要把atomic当成“轻量锁”,而要当成“可见性边界”

五、真实场景

5.1 高性能计数器

5.1.1 Mutex版本(参照)

type Counter struct {
    mu sync.Mutex
    n  int64
}
func (c *Counter) Inc() {
    c.mu.Lock()
    c.n++
    c.mu.Unlock()
}

5.1.2 atomic版本

type Counter struct {
    n int64
}
func (c *Counter) Inc() {
    atomic.AddInt64(&c.n, 1)
}
func (c *Counter) Load() int64 {
    return atomic.LoadInt64(&c.n)
}

适用前提:只有“加”和“读”,不涉及其他状态。

5.2 一次性初始化(比sync.Once更底层)

var initialized int32
func Init() {
    if atomic.CompareAndSwapInt32(&initialized, 0, 1) {
        // 只有一个 goroutine 会进入
        doSomeInit()
    }
}

内存语义保证:CAS成功=发布初始化结果,之后Load看到1=可见所有初始化写

5.3 配置热更新(读多写少)

5.3.1 使用atomic.Value

var cfg atomic.Value // store *Config
func LoadConfig() *Config {
    return cfg.Load().(*Config)
}
func UpdateConfig(c *Config) {
    cfg.Store(c)
}

为什么不用RWMutex?

  • 读路径零锁、零竞争

  • Store = 完整内存屏障

5.4 状态机/标志位

const (
    StateInit = iota
    StateRunning
    StateStopped
)
var state int32
func Start() bool {
    return atomic.CompareAndSwapInt32(&state, StateInit, StateRunning)
}

为什么安全?状态是单变量有限状态机;CAS保证状态跃迁原子性。

六、内存屏障到底在“挡住”什么?

6.1 CPU可能做的事

Store -> Store 乱序

Load -> Load 乱序

Load -> Store 乱序

6.2 atomic 干的事

操作屏障语义
LoadAcquire
StoreRelease
CAS/SwapFull Barrier   

直观理解:atomic = 在这里画一条“所有人都必须停一下”的线

七、常见致命错误

7.1 atomic保护指针字段,但对象内容可变

7.2 用atomic实现复杂临界区

7.3 混用atomic和普通读写,同一变量 要么全atomic,要么全加锁。

八、atomic和Mutex怎么选?

场景推荐
单变量计数/标志atomic
多变量一致性mutex
配置快照atomic.Value
复杂状态机mutex
高竞争临界区mutex

九、总结

atomic是并发里的手术刀,不是万能的瑞士军刀;它解决的是可见性与有序性,不是复杂同步;用对场景,性能和可预测性都极强。

哲理:成熟不是控制一切,而是接受不可控,只守住最关键的状态。

*源码地址*

1、公众号“Codee君”回复“每日一Go”获取源码

2、pan.baidu.com/s/1B6pgLWfS… 


如果您喜欢这篇文章,请您(点赞、分享、亮爱心),万分感谢!