Có một mẫu cực phổ biến trong dịch vụ Go: một cấu hình (hoặc bảng định tuyến, danh sách feature flag) được đọc hàng triệu lần mỗi giây bởi nhiều goroutine, nhưng cập nhật rất hiếm. Dùng mutex để bảo vệ nó là lãng phí — mỗi lần đọc phải khóa. atomic.Pointer giải quyết đúng chỗ này: đọc một con trỏ chia sẻ nguyên tử, không khóa, và đo được nhanh hơn RWMutex tới ~870 lần. Bài này mổ xẻ cơ chế và đo thật.
atomic.Pointer: hoán đổi cả cấu hình bằng một lệnh
atomic.Pointer[T] (Go 1.19+, generic type-safe) cho phép đọc/ghi một con trỏ *T nguyên tử:
var cfg atomic.Pointer[Config]
cfg.Store(&Config{Timeout: 30}) // đặt con trỏ nguyên tử
c := cfg.Load() // đọc không khóa, trả *Config trực tiếp
// đổi CẢ cấu hình bằng MỘT lệnh atomic — reader thấy cũ HOẶC mới, không nửa vời
cfg.Store(&Config{Timeout: 60})
Điểm cốt lõi là tính nguyên tử của con trỏ: reader luôn nhận một Config nhất quán — hoặc con trỏ cũ (trỏ struct cũ trọn vẹn) hoặc con trỏ mới (struct mới trọn vẹn), không bao giờ thấy một struct đang sửa dở. Đây là mẫu copy-on-write: muốn đổi, tạo một Config mới hoàn chỉnh rồi Store con trỏ mới trong một thao tác.

Hình 1: atomic.Pointer hot-swap cấu hình, CompareAndSwap cập nhật có điều kiện, và so sánh với atomic.Value (API cũ cần type-assert, panic nếu Store sai kiểu).
CompareAndSwap: nền tảng lock-free
Ngoài Store/Load, CompareAndSwap cho cập nhật có điều kiện:
old := cfg.Load()
moi := &Config{Timeout: 90}
ok := cfg.CompareAndSwap(old, moi) // chỉ đổi NẾU con trỏ vẫn là old
// ok=false nếu ai đó đã đổi giữa chừng → thử lại
CAS chỉ ghi nếu con trỏ hiện tại đúng như bạn mong đợi. Nếu một goroutine khác đã đổi nó giữa lúc bạn Load và lúc CAS, CAS trả false và bạn thử lại. Đây là nền tảng của mọi cấu trúc dữ liệu lock-free (chủ đề bài sau).
Đo thật: nhanh hơn RWMutex ~870 lần
So ba cách đọc một cấu hình song song trên 10 core (mẫu đọc-nhiều thực tế):

Hình 2: Đọc song song 10 core — atomic.Pointer.Load 0,082 ns so với Mutex 57,6 ns và RWMutex 71,6 ns. atomic.Pointer nhanh hơn ~870 lần; và chạy thật hot-swap/CAS/panic của atomic.Value.
- atomic.Pointer.Load: 0,082 ns/op.
- Mutex (Lock/Unlock): 57,6 ns/op.
- RWMutex (RLock/RUnlock): 71,6 ns/op.
atomic.Pointer nhanh hơn RWMutex ~870 lần khi đọc song song. Vì sao khác biệt khủng khiếp vậy? Load chỉ là một atomic load con trỏ — không ghi vào bộ nhớ chia sẻ, nên mỗi core đọc độc lập, không tranh chấp cache, mở rộng tuyến tính theo số core. Mutex/RWMutex phải ghi vào một biến khóa chung (đếm reader, cờ lock), khiến cache-line "nảy" qua lại giữa các core — điểm nghẽn kinh điển.
Đáng chú ý: RWMutex ở đây còn chậm hơn Mutex. Với critical section siêu ngắn (đọc một field), overhead quản lý reader-count của RWMutex lớn hơn lợi ích cho phép đọc song song — một sự thật ngược trực giác đã đo được.
Ứng dụng thực tế
Cấu hình/feature flag hot-reload. Đây là use case số một: load config từ file/API, Store vào atomic.Pointer, mọi goroutine Load không khóa. Khi config đổi, tạo struct mới và Store — reader tự nhận bản mới ở lần Load kế. Không downtime, không khóa.
Bảng định tuyến / snapshot dữ liệu đọc-nhiều. Bất cứ dữ liệu nào đọc rất nhiều, cập nhật hiếm, và có thể thay toàn bộ bằng một con trỏ mới (copy-on-write) đều hợp: routing table, danh sách server, cache metadata. Cập nhật tốn (copy cả cấu trúc) nhưng đọc cực rẻ — đánh đổi đúng cho tỷ lệ đọc/ghi cao.
Ưu tiên atomic.Pointer[T] hơn atomic.Value. atomic.Value (API cũ) lưu any, cần type-assert khi Load và panic nếu Store một kiểu khác lần trước (đo thật: store of inconsistently typed value into Value). atomic.Pointer[T] type-safe, không cần assert, an toàn hơn — dùng nó trừ khi cần lưu giá trị không-con-trỏ (hiếm).
Đánh đổi cần cân nhắc
atomic.Pointer chỉ hợp copy-on-write, không hợp cập nhật từng phần. Nó thay toàn bộ con trỏ. Nếu bạn cần sửa một phần của cấu trúc chia sẻ (tăng một field, thêm vào map), atomic.Pointer không giúp — bạn phải copy cả cấu trúc rồi swap, tốn nếu cấu trúc lớn hoặc cập nhật thường xuyên. Khi ghi nhiều, mutex hoặc sync.Map có thể hợp hơn.
Con trỏ cũ vẫn sống chừng nào còn reader giữ. Sau Store con trỏ mới, các reader đã Load con trỏ cũ vẫn dùng struct cũ an toàn (GC giữ nó sống tới khi không ai tham chiếu). Đây là ưu điểm (không cần đồng bộ giải phóng) nhưng cũng nghĩa là struct cũ chưa được thu ngay — cân nhắc nếu struct rất lớn và swap liên tục.
Đừng thay mọi mutex bằng atomic. atomic.Pointer thắng lớn cho đọc-nhiều-ghi-hiếm copy-on-write. Với logic cần bảo vệ nhiều biến cùng lúc, hoặc critical section phức tạp, mutex vẫn đúng và dễ đúng hơn. Chọn atomic khi mẫu truy cập khớp, không phải vì "atomic nhanh hơn".
Ba ý mang về
- atomic.Pointer[T] cho đọc/ghi con trỏ chia sẻ nguyên tử, type-safe (Go 1.19+): hot-swap cả cấu hình bằng một
Store(reader thấy cũ hoặc mới trọn vẹn, không nửa vời — copy-on-write), vàCompareAndSwapcho cập nhật có điều kiện (nền tảng lock-free). - Đọc bằng atomic.Pointer nhanh hơn RWMutex ~870 lần khi song song: đo thật Load 0,082 ns so với RWMutex 71,6 ns trên 10 core — vì Load không ghi bộ nhớ chia sẻ nên không tranh chấp cache, mở rộng tuyến tính; RWMutex thậm chí chậm hơn Mutex cho critical section siêu ngắn.
- Ưu tiên atomic.Pointer[T] hơn atomic.Value cũ: atomic.Value lưu
any, cần type-assert và panic nếu Store kiểu không nhất quán — nhưng atomic.Pointer chỉ hợp copy-on-write (thay toàn bộ con trỏ), không hợp cập nhật từng phần hay ghi nhiều.
Phần sau ta đi sâu vào chính nguyên thủy nền tảng vừa nhắc: Phần sau mổ xẻ CAS (Compare-And-Swap) và cấu trúc lock-free — cách xây một stack/counter không khóa bằng vòng lặp CAS, vấn đề ABA, và khi nào lock-free thực sự thắng mutex.