sync.Map thường bị hiểu nhầm là "map an toàn luồng nhanh hơn" — thay thế mọi map + mutex. Sai. Tài liệu Go nói thẳng: nó tối ưu cho hai kịch bản cụ thể, và ngoài đó map + Mutex thường tốt hơn. Bí mật nằm ở cấu trúc bên trong: một map read không khóa cho đường nhanh, và một map dirty có mutex cho key mới. Bài này mổ xẻ cơ chế hai tầng đó và đo thật khi nào nó thắng, khi nào chỉ hòa.
Cấu trúc hai tầng: read và dirty
Bên trong, sync.Map giữ hai map:
type Map struct {
mu Mutex
read atomic.Pointer[readOnly] // đọc KHÔNG khóa (phần lớn truy cập)
dirty map[any]*entry // key mới, có Mutex bảo vệ
misses int // đếm lần đọc trượt read → dirty
}
read: một ảnh chụp bất biến, đọc bằng atomic — không cần khóa. Đây là đường nhanh.dirty: chứa các key chưa có trongread(mới thêm); mọi truy cập cầnMutex.misses: đếm số lần đọc trượtreadphải xuốngdirty. Khi trượt quá nhiều,dirtyđược thăng cấp thànhreadmới.

Hình 1: Cấu trúc hai tầng. Load thử read trước (không khóa); trượt thì xuống dirty (có khóa) và đếm miss; miss đủ nhiều thì thăng dirty→read. Store key cũ dùng CAS, key mới qua dirty.
Load và Store hoạt động thế nào
Load thử read trước — đường nhanh không khóa:
func Load(k):
if k trong read { return value } // KHÔNG khóa → cực nhanh
// trượt read:
mu.Lock()
if k trong dirty { misses++; return value }
if misses >= len(dirty) { thăng dirty → read } // dọn định kỳ
mu.Unlock()
Store key đã có trong read dùng CAS (đường nhanh); key mới phải qua dirty dưới mutex. Đây chính là điểm mấu chốt: đọc key có sẵn không bao giờ khóa, nên đọc-nhiều cực nhanh; nhưng thêm key mới luôn khóa, nên ghi-nhiều không có lợi thế.
Đo thật: thắng lớn đọc-nhiều, hòa ghi-nhiều
Hai kịch bản song song trên 10 core, 1000 key:

Hình 2: Đọc-nhiều — sync.Map 2,98 ns so với RWMutex 89,3 ns (~30x). Ghi-nhiều — sync.Map 89,3 ns so với RWMutex 98,9 ns (gần bằng). Bảng chọn cấu trúc.
- Đọc-nhiều: sync.Map 2,98 ns so với map+RWMutex 89,3 ns — nhanh hơn ~30 lần. Load trúng
readkhông khóa, mỗi core đọc độc lập, không tranh chấp cache. RWMutex phải đụng biến khóa chung (đếm reader), cache-line nảy giữa core. - Ghi-nhiều: sync.Map 89,3 ns so với RWMutex 98,9 ns — gần bằng nhau. Store key mới phải qua
dirtydưới mutex cộng chi phí thăng cấp, nên không còn lợi thế; đôi khi còn thuamap + Mutextùy mẫu.
Hai kịch bản sync.Map được thiết kế cho
Tài liệu Go nêu rõ hai ca sync.Map tối ưu:
- Key ghi một lần, đọc nhiều lần: cache đọc-nhiều, config, bảng tra cứu ít đổi. Sau khi key vào
read, mọi đọc là không khóa. - Nhiều goroutine với tập key rời nhau: mỗi goroutine thao tác trên key riêng, ít đụng nhau — giảm tranh chấp trên
dirty.
Ngoài hai ca này, map + Mutex/RWMutex thường tốt hơn — đặc biệt khi ghi thường xuyên hoặc key mới liên tục.
Ứng dụng thực tế
Dùng sync.Map cho cache/registry đọc-nhiều. Một bảng ánh xạ id→object, kết nối→session, hoặc feature flag mà đọc áp đảo ghi — đây là đất diễn của sync.Map. Load không khóa cho hiệu năng đọc gần như hoàn hảo dưới song song, như đo thấy ~30x RWMutex.
Dùng map + Mutex cho mọi thứ khác. Nếu ghi thường xuyên, cần biết size chính xác (sync.Map không có Len() O(1)), hoặc cần thao tác phức tạp (duyệt rồi sửa nguyên tử), map thường với mutex đơn giản hơn và thường nhanh hơn hoặc bằng. Đừng mặc định dùng sync.Map vì nghe "concurrent".
Cẩn thận API khác biệt. sync.Map dùng Load/Store/Delete/Range/LoadOrStore với any — không type-safe (phải type-assert), không dùng được [k], không len(), Range không đảm bảo ảnh chụp nhất quán. API rườm rà hơn map thường — thêm một lý do chỉ dùng khi thật cần.
Đánh đổi cần cân nhắc
sync.Map đánh đổi bộ nhớ và độ phức tạp lấy tốc độ đọc. Nó giữ hai bản sao dữ liệu (read + dirty) trong giai đoạn chuyển tiếp, và cơ chế thăng cấp/expunge tốn bộ nhớ hơn một map đơn. Với dữ liệu nhỏ hoặc ghi nhiều, chi phí này không đáng — map+mutex gọn hơn.
Ghi key mới liên tục làm dirty "thrash". Mỗi key mới đi vào dirty, và khi read bị coi là lỗi thời, cả dirty được copy sang read — chi phí O(n) theo số key. Nếu workload liên tục thêm key mới (không phải cập nhật key cũ), sync.Map thrash và chậm. Đây là anti-pattern rõ.
Đo trên mẫu tải thật của bạn. Như hai kịch bản cho thấy, lợi ích sync.Map hoàn toàn phụ thuộc tỷ lệ đọc/ghi và mẫu key. "sync.Map nhanh hơn" chỉ đúng cho đọc-nhiều. Luôn benchmark với tỷ lệ đọc/ghi và phân bố key giống production trước khi chọn.
Ba ý mang về
- sync.Map dùng cấu trúc hai tầng: read (atomic, không khóa) + dirty (map có Mutex): Load key có sẵn trúng
readkhông cần khóa (đường nhanh), key mới và trượt read phải quadirtydưới mutex; miss đủ nhiều thìdirtythăng cấp thànhread. - sync.Map thắng lớn khi đọc-nhiều, chỉ hòa khi ghi-nhiều: đo thật đọc-nhiều 2,98 ns so với map+RWMutex 89,3 ns (~30x, nhờ đọc không khóa không tranh chấp cache), nhưng ghi-nhiều 89,3 so 98,9 ns (gần bằng, vì key mới luôn qua dirty + mutex + thăng cấp).
- sync.Map KHÔNG phải "map nhanh hơn" mặc định: nó tối ưu cho hai kịch bản (key ghi-1-đọc-nhiều, hoặc tập key rời nhau giữa goroutine) — ngoài đó map+Mutex thường tốt hơn; ghi key mới liên tục làm dirty thrash, và API
anykhông type-safe, khôngLen()O(1).
Phần sau ta xem một nguyên thủy đồng bộ ít gặp nhưng mạnh cho kiểm soát tài nguyên: Phần sau mổ xẻ semaphore có trọng số (golang.org/x/sync/semaphore) — cách giới hạn số tài nguyên đồng thời có trọng số khác nhau, khác gì với channel làm semaphore, và khi nào cần.