Go 无锁并发实战:用 sync/atomic 与 atomic.Pointer 实现计数器与配置热更新

test12026-10-073 次阅读

互斥锁不是并发的唯一答案

一说到共享状态,很多人的第一反应是加 sync.Mutex。锁能解决所有竞态,但代价是串行化:同一时刻只有一个 goroutine 能进临界区,高并发下容易成为瓶颈。当操作只是"读一个数、加一、写回"这种简单动作时,CPU 提供了原子的读改写指令,Go 的 sync/atomic 包把它封装成了无锁接口,既快又不会阻塞。

原子计数器:比 Mutex 更轻

atomic.Int64 自带 Load、Add、Store,底层是一条 CPU 原子指令,不需要进锁。下面是 1000 个 goroutine 各加一次,最后必然得到 1000。

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

type Counter struct {
	v atomic.Int64
}

func (c *Counter) Inc() int64 { return c.v.Add(1) }
func (c *Counter) Load() int64 { return c.v.Load() }

func main() {
	var c Counter
	var wg sync.WaitGroup
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			c.Inc()
		}()
	}
	wg.Wait()
	fmt.Println(c.Load()) // 1000
}

如果用 Mutex 包一个 int64,结果一样正确,但每次 Inc 都要抢锁释放锁。计数器这种高频操作,原子类型能明显少掉锁的开销。

atomic.Pointer:无锁换配置

配置热更新是个经典场景:后台 goroutine 读到了新配置,要替换掉正在被大量请求读取的旧配置。如果每次读都加锁,读多写少时锁几乎都花在读者互相让路上。atomic.Pointer 让写端一次性原子替换整个指针,读端永远拿到一个完整一致的快照,全程无锁。

package main

import (
	"fmt"
	"sync/atomic"
)

type Config struct {
	TimeoutMs int
	Feature   bool
}

var cfg atomic.Pointer[Config]

func init() {
	cfg.Store(&Config{TimeoutMs: 500, Feature: false})
}

func reload() {
	// 原子替换,读者无锁,且永远看不到写到一半的结构
	cfg.Store(&Config{TimeoutMs: 800, Feature: true})
}

func handle() Config {
	return *cfg.Load() // 取一份完整快照,后续用不会读到半旧半新
}

func main() {
	before := handle()
	reload()
	after := handle()
	fmt.Println(before.TimeoutMs, after.TimeoutMs)
}

为什么这样不会读到"撕裂"的值

指针替换是一个机器字宽的原子写,要么旧指针要么新指针,不存在中间态。读端拿到指针后解引用,得到的要么是旧对象要么是全新对象,两个对象本身都是初始化好的完整值。这和"先改字段 A 再改字段 B"的普通写不同,后者在并发读时可能看到只改了一半的结构。

使用边界

  • atomic 适合单一变量的读改写和指针整体替换;一旦要同时保护多个字段的一致性,还是得用 Mutex。
  • atomic.Pointer 存的是指针,读端解引用后得到的对象不应再被写端原地修改,否则引入新的竞态;正确做法是写端构造新对象再整体替换。
  • Go 1.19 之后推荐用 atomic.Int64、atomic.Bool 这类类型安全封装,比老式的 atomic.AddInt64(&x, 1) 更不容易用错地址。
  • 无锁不等于无成本,它只是把成本从锁的等待转移到了缓存一致性流量上;在"一个变量高频读写"的场景收益最大。

小结

并发不只有加锁一条路。sync/atomic 的整型类型适合计数器、标志位这类单值高频操作;atomic.Pointer 适合读多写少、需要整体替换的配置或缓存对象。把对的工具用在对的场景,既保住正确性,又把锁竞争这座大山搬走。

T

test1

文章作者

互斥锁不是并发的唯一答案 一说到共享状态,很多人的第一反应是加 sync.Mutex。锁能解决所有竞态,但代价是串行化...

分类
技术
发布时间
2026-10-07
字数
约 1921 字
阅读
3 次