Day 18|sync/atomic 与无锁编程基础[Go 语言 30 天系统学习计划]

学习目标

  • 理解 atomic 在 Go 并发模型中的定位
  • 掌握常见原子操作的正确使用方式
  • 理解内存可见性与顺序保证的基本概念
  • 避免“滥用无锁”的工程误区

一、为什么需要 sync/atomic

在 Day 17 中你已经看到:

  • mutex 可以解决大多数并发安全问题
  • 但 mutex 有锁竞争和上下文切换成本

sync/atomic 提供的是:

  • 更轻量的并发原语
  • 基于 CPU 原子指令
  • 避免显式加锁

重要认知:

atomic 是为“非常小的共享状态”准备的工具,而不是 mutex 的替代品。


二、什么是原子操作

原子操作指的是:

在并发场景下,一个操作要么完整执行,要么完全不执行,中间状态对其它 goroutine 不可见。

示例问题:

count++

这并不是原子操作,而是:

  • 读取 count
  • 加 1
  • 写回 count

atomic 提供的是“一条指令级别”的安全操作。


三、atomic 包的基本操作

atomic 位于:

import "sync/atomic"

常见操作:

原子加:

atomic.AddInt64(&counter, 1)

原子读:

v := atomic.LoadInt64(&counter)

原子写:

atomic.StoreInt64(&counter, 10)

关键规则:

  • 参数必须是指针
  • 变量必须是 64 位对齐(Go 会保证)

四、CAS(Compare-And-Swap)机制

CAS 是 atomic 的核心思想之一。

含义:

“如果当前值等于期望值,则更新为新值”

示例:

ok := atomic.CompareAndSwapInt64(&x, old, new)

返回值:

  • true:交换成功
  • false:当前值不等于 old

CAS 是实现:

  • 无锁计数器
  • 状态机
  • 轻量级同步

的基础。


五、atomic.Value:原子存取复杂对象

atomic.Value 用于原子地存取一个值(interface{})。

示例:

var v atomic.Value

v.Store(map[string]int{"a": 1})

m := v.Load().(map[string]int)

使用规则:

  • 第一次 Store 决定类型
  • 之后 Store 必须是同一具体类型
  • Load 永远是原子的

工程场景:

  • 配置热更新
  • 读多写少的共享对象

六、atomic 与内存可见性

atomic 操作不仅是“不可分割”,还保证:

  • 内存可见性
  • 一定程度的顺序性

这意味着:

  • 一个 goroutine 的 atomic.Store
  • 对另一个 goroutine 的 atomic.Load 是可见的

工程结论:atomic 是 Go 并发内存模型的一部分,而不是简单的工具函数。


七、atomic 的典型使用场景

  • 计数器(QPS、请求数)
  • 状态标志位(启动/关闭)
  • 读多写少的全局配置(atomic.Value)

不适合 atomic 的场景:

  • 复杂临界区
  • 多个字段需要一致性
  • 涉及条件判断与流程控制

八、atomic vs mutex 的工程取舍

选择 atomic 当:

  • 操作极其简单
  • 性能瓶颈明确在锁竞争
  • 代码可读性仍然可接受

选择 mutex 当:

  • 逻辑复杂
  • 需要维护不变量
  • 团队可维护性优先

工程共识:

能用 mutex 清晰解决的问题,不要强行 atomic。


九、无锁编程的常见误区

  • 认为无锁一定比加锁快
  • 多个 atomic 操作组合成“伪原子逻辑”
  • 忽视代码可读性与维护成本

工程结论:无锁是高级工具,不是默认选择。


十、atomic 总结

  • atomic 提供底层、轻量级并发原语
  • atomic 解决的是“单点状态”的并发安全
  • atomic 是 mutex 的补充,而不是替代

一句话总结:

atomic 用得好是利器,用不好是灾难。


预告:Day 19|内存模型与 happens-before 关系

下一天将系统讲解:

  • Go 内存模型的核心规则
  • happens-before 的真实含义
  • 为什么“看起来正确”的并发代码仍然是错的
  • 如何写出符合内存模型的代码

发表评论