Chuyển đến nội dung

Hoạt động nguyên tử & Thứ tự bộ nhớ

Hiểu lập trình không khóa với std::atomic. Thứ tự bộ nhớ chính (thư giãn, thu thập, giải phóng, seq_cst), triển khai khóa xoay với Atomic_flag và tìm hiểu mẫu so sánh và hoán đổi (CAS).

Hoạt động nguyên tử là gì?

Hoạt động nguyên tử là không thể phân chia được — nó hoàn thành hoàn toàn hoặc không hoàn thành, không có trạng thái trung gian nào được hiển thị cho các luồng khác.std::atomiccung cấp quyền truy cập nguyên tử vào giá trị thuộc loạiTmà không yêu cầu mutex.

Hãy xem xét++counterđơn giản trênintthông thường được chia sẻ giữa các luồng. Đây thực sự là ba bước: tải, tăng, lưu trữ. Một luồng khác có thể xen kẽ giữa các bước này, gây ra cuộc chạy đua dữ liệu (hành vi không xác định). Vớistd::atomic, mức tăng là một thao tác nguyên tử duy nhất — không xen kẽ, không UB.

Nhưng nguyên tử còn đi sâu hơn chỉ là “các hoạt động không thể phân chia”. Chúng cũng kiểm soát thứ tự bộ nhớ — các quy tắc chi phối thời điểm ghi bởi một luồng sẽ hiển thị với các luồng khác. Đây là nơi nguyên tử trở nên mạnh mẽ và tinh tế.

std::atomic cơ bản

Các hoạt động nguyên tử phổ biến nhất làload(),store(),exchange()vàfetch_add()/fetch_sub(). Bạn cũng có thể sử dụng các toán tử++,--,+=, v.v.

atomic_counter.cpp
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>

std::atomic<int> counter{0};  // atomic integer, initialized to 0

void increment(int n) {
    for (int i = 0; i < n; ++i) {
        counter.fetch_add(1, std::memory_order_relaxed);
        // or simply: ++counter;  (uses seq_cst by default)
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(increment, 100'000);
    }
    for (auto& t : threads) t.join();

    std::cout << "Counter: " << counter.load() << "\n";
    // Always prints 1000000 — no data race, no mutex needed
    return 0;
}

Giải thích thứ tự bộ nhớ

CPU và trình biên dịch hiện đại sắp xếp lại hoạt động bộ nhớ để đạt hiệu suất. Trên x86, điều này tương đối nhẹ, nhưng trên ARM/RISC-V thì rất mạnh. Thứ tự bộ nhớ cho trình biên dịch và CPU biết việc sắp xếp lại nào được phép.

memory_order_seq_cst (mặc định) — Thứ tự mạnh nhất. Tất cả các luồng đều xem tất cả các hoạt độngseq_csttheo tổng thứ tự giống nhau. Đơn giản và an toàn nhưng có thể hạn chế tối ưu hóa.

memory_order_acquire — Được sử dụng trên tải. Không có bộ nhớ nào đọc/ghi trong luồng hiện tại có thể được sắp xếp lại trước lần tải này. Hãy nghĩ về điều đó như sau: "mọi thứ tôi đọc sau thời điểm này ít nhất cũng thấy được những gì chủ đề phát hành đã viết."

memory_order_release — Được sử dụng trên cửa hàng. Không có bộ nhớ nào đọc/ghi trong luồng hiện tại có thể được sắp xếp lại sau lần lưu trữ này. Hãy nghĩ về nó như sau: "mọi thứ tôi viết trước thời điểm này đều hiển thị với chuỗi thu thập."

memory_order_acq_rel — Kết hợp thu thập và giải phóng. Được sử dụng trong các hoạt động đọc-sửa-ghi nhưcompare_exchange.

memory_order_relaxed — Không đảm bảo thứ tự nào vượt quá tính nguyên tử. Hoạt động mang tính nguyên tử nhưng có thể được sắp xếp lại một cách tự do. Chỉ sử dụng cho các bộ đếm hoặc số liệu thống kê độc lập khi việc đặt hàng không quan trọng.

Có được-Phát hành trong thực tế

Mẫu phát hành thu được là đặc điểm của lập trình không khóa. Cửa hàng phát hành trong một luồng sẽ đồng bộ hóa với tải thu được trong một luồng khác, thiết lập mối quan hệ xảy ra trước đó.

acquire_release.cpp
#include <iostream>
#include <atomic>
#include <thread>
#include <cassert>

std::atomic<bool> data_ready{false};
int payload = 0;  // non-atomic — protected by acquire-release

void producer() {
    payload = 42;  // write payload BEFORE the release store
    data_ready.store(true, std::memory_order_release);
    // release: all writes before this store are visible
    // to a thread that does an acquire load of data_ready
}

void consumer() {
    while (!data_ready.load(std::memory_order_acquire)) {
        // spin — acquire: sees all writes before the release store
    }
    // guaranteed to see payload == 42
    assert(payload == 42);  // never fires
    std::cout << "Payload: " << payload << "\n";
}

int main() {
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join();
    t2.join();
    return 0;
}

Mẫu so sánh và hoán đổi (CAS)

So sánh và trao đổi (compare_exchange_weak/compare_exchange_strong) là khối xây dựng cơ bản của cấu trúc dữ liệu không khóa. Nó kiểm tra một cách nguyên tử xem giá trị có bằng giá trị mong đợi hay không và nếu có thì thay thế nó bằng giá trị mong muốn.

cas_max.cpp
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>

std::atomic<int> max_value{0};

void update_max(int new_val) {
    int current = max_value.load(std::memory_order_relaxed);
    // CAS loop: keep trying until we succeed or new_val is not bigger
    while (new_val > current) {
        if (max_value.compare_exchange_weak(
                current,    // expected (updated on failure)
                new_val,    // desired
                std::memory_order_relaxed)) {
            break;  // success
        }
        // On failure, 'current' is updated with the actual value
        // Loop re-checks if new_val > current (maybe another thread won)
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 100; ++i) {
        threads.emplace_back(update_max, i);
    }
    for (auto& t : threads) t.join();

    std::cout << "Max: " << max_value.load() << "\n";  // always 99
    return 0;
}

Spinlock với std::atomic_flag

std::atomic_flaglà loại nguyên tử nguyên thủy nhất — được đảm bảo không khóa trên mọi nền tảng. Nó chỉ hỗ trợtest_and_set()vàclear(), khiến nó trở nên hoàn hảo cho một spinlock đơn giản.

spinlock.cpp
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>

class SpinLock {
    std::atomic_flag flag_ = ATOMIC_FLAG_INIT;
public:
    void lock() {
        while (flag_.test_and_set(std::memory_order_acquire)) {
            // spin — consider adding a pause/yield hint:
            // __builtin_ia32_pause() or std::this_thread::yield()
        }
    }
    void unlock() {
        flag_.clear(std::memory_order_release);
    }
};

SpinLock spin;
int shared_data = 0;

void work(int n) {
    for (int i = 0; i < n; ++i) {
        spin.lock();
        ++shared_data;  // protected by spinlock
        spin.unlock();
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 4; ++i) {
        threads.emplace_back(work, 100'000);
    }
    for (auto& t : threads) t.join();
    std::cout << "shared_data: " << shared_data << "\n";
    // prints 400000
    return 0;
}
Lỗi thường gặp

memory_order_relaxedcung cấp không đảm bảo đặt hàng ngoài tính nguyên tử. Việc sử dụng nó để bảo vệ dữ liệu phi nguyên tử được chia sẻ (như cờ bảo vệ tải trọng) là một lỗi:

// BUG: relaxed store/load does not synchronize payload
payload = 42;
ready.store(true, memory_order_relaxed);  // might be reordered BEFORE payload=42!

// Consumer:
while (!ready.load(memory_order_relaxed)) {}
assert(payload == 42);  // MAY FAIL on ARM/POWER

Use memory_order_relaxed only when the atomic variable stands alone and doesn't need to synchronize access to other data. For publish-consume patterns, use release/acquire.

Thực hành tốt

Bắt đầu vớimemory_order_seq_cst(mặc định) và chỉ nới lỏng các thứ tự yếu hơn khi hồ sơ chứng minh điều đó là cần thiết. Thứ tự bộ nhớ không chính xác tạo ra các lỗi gần như không thể tái tạo trên x86 nhưng lại xuất hiện trên ARM. Ưu tiên các mutex cho trạng thái chia sẻ phức tạp - các nguyên tử dành cho các biến chia sẻ đơn giản và xây dựng các nguyên hàm không khóa. Luôn xác minh các giả định không khóa bằngstd::atomic::is_lock_free().

Điểm chính
  • std::atomiccung cấp các hoạt động không thể phân chia mà không có mutex - an toàn cho việc đọc và ghi đồng thời
  • Thứ tự bộ nhớ kiểm soát cách các luồng ghi trong một luồng hiển thị với các luồng khác - nó không chỉ là về tính nguyên tử
  • Phát hành có được là mẫu chính: cửa hàng phát hành xuất bản dữ liệu, tải có được tiêu thụ nó
  • compare_exchange_weak/strong(CAS) là khối xây dựng cho các thuật toán không khóa
  • std::atomic_flaglà loại duy nhất được đảm bảo không khóa trên tất cả các nền tảng
  • Mặc định làseq_cstvà chỉ yếu đi khi hồ sơ yêu cầu - các lỗi tinh vi cực kỳ khó gỡ lỗi

Câu đố — Kiểm tra kiến thức

(25 XP)

1. `memory_order_acquire`đảm bảo điều gì cho hoạt động tải?

2. Tại sao`compare_exchange_weak`thường được sử dụng trong vòng lặp?

3. Khi nào`memory_order_relaxed`an toàn để sử dụng?