0

Go Zero to Hero - Bài 62: Các thao tác nguyên tử với package sync/atomic

1. Thao tác Nguyên tử (Atomic Operation) là gì?

Bạn còn nhớ ở Bài 58, chúng ta đã phát hiện ra lệnh ticketCount++ thực chất là 3 lệnh CPU riêng biệt (Read -> Modify -> Write) dẫn đến Race Condition?

Thao tác nguyên tử (Atomic) là cách bạn ép CPU ở cấp độ phần cứng (Hardware-level) phải gộp 3 bước đó lại thành 1 bước duy nhất, không thể bị chia cắt hay xen ngang. Nếu CPU đang thực thi lệnh Atomic, toàn bộ các Core khác của máy chủ sẽ bị chặn lại trong một tíc tắc (nano-giây) ở cấp độ chip nhớ, đảm bảo không ai có thể can thiệp.

Vì nó khóa ở cấp độ phần cứng (Hardware Lock) thay vì cấp độ phần mềm (OS Lock) như Mutex, nên nó có tốc độ nhanh hơn Mutex từ 10 đến 50 lần.


2. Kỷ nguyên mới: Go 1.19 và các kiểu dữ liệu Atomic chuẩn mực

Trước bản Go 1.19, việc dùng package atomic khá cồng kềnh vì phải truyền con trỏ và gọi hàm như atomic.AddInt64(&counter, 1). Rất nhiều người lỡ tay gán trực tiếp counter = 5 dẫn đến hỏng bét hệ thống.

Từ Go 1.19 trở đi, Golang đã giới thiệu các Struct Atomic bọc sẵn (như atomic.Int64, atomic.Bool, atomic.Uint32), giúp code vừa sạch (Clean Architecture) vừa type-safe (an toàn kiểu dữ liệu).

Hãy xem cách chúng ta đếm số lượng vé thành công một cách thần tốc:

package main

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

func main() {
    // Khai báo biến đếm Atomic (thay vì int64 thông thường)
    var successTickets atomic.Int64 
    var wg sync.WaitGroup

    // Giả lập 500 cổng xoay AFC, mỗi cổng quẹt 100 vé cùng lúc
    for i := 0; i < 500; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for j := 0; j < 100; j++ {
                // 1. TĂNG BIẾN ĐẾM (Nguyên tử)
                // Nhanh hơn mu.Lock() + count++ + mu.Unlock() rất nhiều!
                successTickets.Add(1)
            }
        }()
    }

    wg.Wait()
    
    // 2. ĐỌC BIẾN ĐẾM (Nguyên tử)
    // Đọc cũng phải dùng hàm Load() để tránh lấy nhầm rác bộ nhớ
    fmt.Println("Tổng số vé thành công:", successTickets.Load()) 
    // Kết quả luôn chuẩn xác: 50000
}

3. Các chiêu thức thực chiến với kiểu Atomic

Ngoài lệnh Add() và Load(), bộ Struct của sync/atomic còn hỗ trợ:

  • Store(value): Ghi đè một giá trị mới một cách an toàn. (VD: Cập nhật cấu hình Global).
  • Swap(new): Ghi giá trị mới vào, và trả về cho bạn giá trị cũ trước khi bị ghi đè.

Đặc biệt: Khởi tạo cờ trạng thái cực nhanh với atomic.Bool

Rất hữu dụng để làm công tắc "Tắt/Mở" Server (Graceful Shutdown) hoặc đánh dấu hệ thống đang bảo trì.

var isMaintenance atomic.Bool

func checkStatus() {
    if isMaintenance.Load() {
        fmt.Println("Server đang bảo trì, vui lòng quay lại sau!")
        return
    }
    fmt.Println("Xử lý giao dịch...")
}

func triggerMaintenance() {
    // Bật cờ bảo trì an toàn trên mọi luồng
    isMaintenance.Store(true) 
}

4. Tuyệt kỹ Backend: CAS (Compare-And-Swap)

Đây là kỹ thuật đỉnh cao nhất của sync/atomic và là trái tim của các cơ chế Khóa lạc quan (Optimistic Locking) trong Database hay Distributed System.

Lệnh CompareAndSwap(old, new) hoạt động theo logic: "Tôi chỉ cập nhật biến này thành giá trị MỚI, NẾU VÀ CHỈ NẾU giá trị hiện tại của nó vẫn đang đúng bằng giá trị CŨ mà tôi dự đoán. Nếu ai đó đã lén thay đổi nó rồi, phép gán sẽ thất bại."

Hãy xem kịch bản "Chỉ cho phép duy nhất 1 luồng trở thành Leader để gửi báo cáo cuối ngày":

package main

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

func main() {
    // Biến trạng thái: 0 là Chưa gửi, 1 là Đang gửi/Đã gửi
    var reportStatus atomic.Int32 
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go func(workerID int) {
            defer wg.Done()
            
            // Lệnh CAS (Compare And Swap)
            // Cố gắng đổi từ 0 (Chưa gửi) sang 1 (Đã gửi)
            if reportStatus.CompareAndSwap(0, 1) {
                // Nếu thành công (trước đó đúng là 0), luồng này là người chiến thắng!
                fmt.Printf("🏆 [Worker %d] Giành được quyền Leader! Đang tạo báo cáo chốt ca...\n", workerID)
            } else {
                // Nếu thất bại (ai đó đã đổi nó thành 1 rồi), luồng này ngậm ngùi rút lui.
                fmt.Printf("💤 [Worker %d] Đã có người làm. Tôi đi ngủ đây.\n", workerID)
            }
        }(i)
    }

    wg.Wait()
}

Kết quả: Chỉ có đúng 1 Worker in ra chữ 🏆, các luồng khác sẽ in ra chữ 💤. Không cần Mutex, không bị Block, tốc độ xử lý ở ngưỡng tuyệt đối!


5. Ranh giới giữa Mutex và Atomic

Biết sức mạnh của Atomic, nhiều người sẽ có xu hướng vứt bỏ Mutex. Nhưng hãy cẩn trọng, đây là bảng ranh giới bạn cần khắc cốt ghi tâm:

Tiêu chí sync/atomic sync.Mutex / sync.RWMutex
Bản chất Khóa phần cứng (CPU). Khóa phần mềm (OS, Goroutine Scheduler).
Hiệu năng Cực kỳ nhanh, không gây khóa luồng (Non-blocking). Chậm hơn, gây khóa luồng (Context Switching).
Giới hạn CHỈ thao tác được trên 1 biến duy nhất (Số, Bool, Con trỏ). Bảo vệ được một khối logic phức tạp chứa hàng chục biến.
Thực chiến Biến đếm (Counter), Cờ trạng thái (Flag), Optimistic Lock. Mảng (Slice), Bản đồ (Map), Thao tác kết nối DB/Network.

💡 Tổng kết

Bạn đã sở hữu trong tay phương pháp đồng bộ hóa ở cấp độ thấp nhất của Go:

  • Siêu tốc: Dùng sync/atomic thay thế Mutex cho các biến đếm hoặc cờ trạng thái đơn giản để tối đa hóa lượng Request/Second (RPS).
  • Chuẩn mực Go 1.19+: Luôn sử dụng các Struct như atomic.Int64, atomic.Bool thay vì các hàm rời rạc cũ kỹ.
  • Chiêu thức CAS: Khái niệm cốt lõi để xây dựng các thuật toán Lock-free (không cần khóa) và chống ghi đè dữ liệu.

All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.