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.

Ảnh chụp đoạn mã Go nền tối minh hoạ atomic.Pointer và atomic.Value hoán đổi trạng thái không khóa, đọc ghi một con trỏ chia sẻ nguyên tử không mutex hoàn hảo cho cấu hình đọc nhiều cập nhật hiếm, một atomic.Pointer T hot-swap cả cấu hình var cfg atomic.Pointer Config cfg.Store Config Timeout 30 đặt con trỏ nguyên tử c bằng cfg.Load đọc không khóa trả con trỏ Config đổ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 reader luôn thấy một Config nhất quán con trỏ cũ hoặc mới trọn vẹn không bao giờ thấy struct đang sửa dở đây là mẫu copy-on-write, hai CompareAndSwap cập nhật có điều kiện old bằng cfg.Load moi bằng Config Timeout 90 ok bằng 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 nền tảng lock-free, ba atomic.Value API cũ có cạm bẫy kiểu var v atomic.Value v.Store Config c bằng v.Load chấm sao Config phải type-assert lưu any v.Store chuoi PANIC kiểu khác lần Store trước store of inconsistently typed value into Value atomic.Value lưu any mọi Store phải cùng kiểu cụ thể sai là panic Load phải type-assert, bốn chọn cái nào atomic.Pointer T Go 1.19 cộng type-safe không cần assert ưu tiên atomic.Value cũ hơn dùng khi cần lưu giá trị không con trỏ hiếm

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ế):

Ảnh chụp bảng kết quả đo thật nền tối atomic.Pointer đọc nhanh hơn RWMutex khoảng 870 lần go test bench cpu song song RunParallel Go 1.23 arm64 10 core đọc config nhiều, đọc config song song trên 10 core cách đọc atomic.Pointer.Load 0,082 ns/op chỉ là atomic load con trỏ mỗi core độc lập Mutex.Lock Unlock 57,6 ns tuần tự hóa hoàn toàn RWMutex.RLock RUnlock 71,6 ns còn chậm hơn Mutex cho critical section siêu ngắn atomic.Pointer.Load nhanh hơn RWMutex khoảng 870 lần 0,082 so 71,6 ns khi đọc song song Load không ghi vào bộ nhớ chia sẻ nên không tranh chấp cache mở rộng tuyến tính theo số core RWMutex Mutex phải đụng một biến khóa chung cache-line nảy giữa các core, chạy thật hot-swap cộng CAS cộng cạm bẫy Value atomic.Pointer type-safe timeout 30 không cần type-assert sau Store mới timeout 60 hot-swap nguyên tử CAS thành công true timeout 90 đổi có điều kiện atomic.Value timeout 10 cần chấm sao Config PANIC store of inconsistently typed value into Value, cốt lõi atomic.Pointer T đọc ghi con trỏ nguyên tử type-safe Go 1.19 cộng đọc song song 0,082 ns vs RWMutex 71,6 ns khoảng 870x không tranh chấp mẫu dùng config state đọc nhiều cập nhật hiếm copy-on-write CAS cập nhật có điều kiện nền tảng lock-free atomic.Value cũ phải type-assert cộng panic nếu Store kiểu khác

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ề

  1. 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à CompareAndSwap cho cập nhật có điều kiện (nền tảng lock-free).
  2. Đọ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.
  3. Ư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.