跳到主要内容

Go 中的 atomic 的使用场景

· 阅读需 4 分钟
素明诚
Full stack development

atomic 的最佳实践和使用场景

计数器和统计信息

原子包常用于实现高性能的计数器或统计数据收集,例如计算网站访问量、服务调用次数等。因为这些操作通常涉及单个变量的简单增减,使用原子操作比引入互斥锁更为高效。

var visitors int64
// 每次用户访问时,增加访客数量
atomic.AddInt64(&visitors, 1)

状态标志管理

在多线程环境中,使用原子操作来检查或设置状态标志可以避免使用锁,特别是在状态变化不频繁,但检查频繁的场景。

var state int32
// 设置状态
atomic.StoreInt32(&state, 1)
// 检查状态
if atomic.LoadInt32(&state) == 1 {
// 执行某些操作
}

无锁数据结构

在实现无锁数据结构,如无锁队列、无锁栈等时,原子操作是必不可少的工具。这些数据结构通常用在高性能或实时系统中,可以显著减少线程阻塞。

type AtomicInt struct {
value int64
}

func (a *AtomicInt) Increment() {
atomic.AddInt64(&a.value, 1)
}

func (a *AtomicInt) Get() int64 {
return atomic.LoadInt64(&a.value)
}

不建议使用的场景

复杂状态或多变量协同

当操作涉及到多个变量或者状态较为复杂的同步时,仅使用原子操作往往无法有效管理。例如,需要根据多个变量的值决定逻辑流程的情况,使用互斥锁可能是更好的选择,因为它们可以同时保护多个变量。

// 错误用法:尝试使用原子操作同步多个变量
var a, b int32
atomic.AddInt32(&a, 1)
atomic.AddInt32(&b, 1)
// 无法保证 a 和 b 的增加操作是同步发生的

复杂的业务逻辑

如果业务逻辑需要条件同步,涉及多步骤验证或者复合状态变化,使用原子操作会使代码复杂且难以维护。在这些场景中,使用更高级的同步机制(如通道、锁或条件变量等)通常更加合适。

高频度的写操作

原子操作确保了单个操作的原子性,即在操作过程中不会被其他线程打断。这通过硬件级别的支持完成,通常是通过一种称为“比较并交换”(Compare-and-Swap, CAS)的操作。当你有多个线程试图同时修改同一个变量时,每个线程都会尝试执行 CAS 操作:

  1. 读取当前值:线程首先读取目标变量的当前值。
  2. 计算新值:基于当前值,线程计算新的值。
  3. 比较并交换:如果目标变量的当前值与步骤 1 中读到的值相同,CAS 操作会将新值写入变量。如果不同,说明在此期间内其他线程已经修改了变量,当前线程的操作失败,通常会重试。

频繁的写操作引发的问题

当大量线程频繁尝试更新同一个变量时,以下问题可能发生

  • 争用高:每个线程都尝试修改数据,但由于 CAS 的工作机制,只有一个线程的修改能成功,其他线程需要重试。这导致了高争用,多个线程不断重试,浪费 CPU 资源。
  • 性能瓶颈:尽管原子操作比锁的开销小,但当争用非常激烈时,线程频繁进行无效的重试,这会成为系统性能的瓶颈。
  • 线程饥饿:在极端的争用场景中,某些线程可能会长时间地成功不了操作,导致线程饥饿。

所以在确保线程安全的同时,优化系统的整体性能,避免因原子操作的过度使用而导致的性能瓶颈。atomic 包在处理简单的同步需求时非常有用,尤其适合于轻量级的操作,比如状态标志或计数器。