簡體   English   中英

在 C++ 和 memory 命令中使用樂觀鎖

[英]Using optimistic locks in C++ and memory order

我想知道“樂觀鎖”(如此處所述: https://db.in.tum.de/~leis/papers/artsync.pdf )如何在 C++ 中用於非原子數據以及什么應使用 memory 命令。

我對上述論文的理解是,以下代碼將同步t1t2 ,而t1將打印兩個更新的變量或不打印。

static constexpr uint64_t locked_bit = (1ULL << 63);

std::atomic<int> data1 = 0;
std::atomic<int> data2 = 0;
std::atomic<uint64_t> versioned_lock = 0;

int main() {
    std::thread t1([&]{
        restart:
        auto s = versioned_lock.load();
        if (s & locked_bit)
            goto restart;

        auto local_data1 = data1.load();
        auto local_data2 = data2.load();
        if (s == versioned_lock.load()) {
            // data has not been overwritten yet, can safely use local_data
            std::cout << local_data1 << " " << local_data2 << std::endl;
        } else {
            // possible race, local_data might be garbage?
        }
    });

    std::thread t2([&]{
        auto current_lock_version = versioned_lock.load();
        current_lock_version++;
        versioned_lock.store(current_lock_version | locked_bit);
        data1.store(1234);
        data2.store(4321);
        versioned_lock.store(current_lock_version);
    });

    t1.join();
    t2.join();
}

我的問題是:

  1. 我這是一個沒有 UB 的有效 C++ 代碼?
  2. 我關於同步的假設是否正確? t1實際上會打印“0 0”或“1234 4321”嗎?
  3. 應該使用哪些 memory 命令來讀/寫versioned_lock 它應該是memory_order_seq_cst還是限制較少的東西? 如果data1data2是非原子的(只是int或什至一些更復雜的數據類型),它也會工作嗎?它是否會影響所需的 memory 順序(或者可能需要 atomic_thread_fence)?

這是沒有 UB 的有效 C++ 代碼嗎?

我認同。 在這樣的程序中獲得 UB 的通常方法是數據競爭,但這只有在並發寫入非原子變量時才會發生,並且該程序中的每個共享變量都是原子的。

我關於同步的假設是否正確? t1 實際上會打印“0 0”或“1234 4321”嗎?

是的。 此版本中的所有加載和存儲都是seq_cst ,因此它們以某種總順序出現,我將使用 < 來表示。 設 L1、L2 表示 t1 中versioned_lock的兩次加載,S1、S2 表示 t2 中的兩次存儲。

如果 S1、S2 都出現在 L1 之前,那么兩個新值都會被看到並且我們打印1234 4321 ,這很好。 如果 S1、S2 都出現在 L2 之后,那么我們打印0 0 ,這也沒有問題。 如果 S1 或 S2 出現在 L1 和 L2 之間,那么 L1 和 L2 將返回不同的值,我們不會打印任何內容,這也沒有問題。

剩下的唯一順序是 S1 < L1 < L2 < S2。 在這種情況下,L1 返回設置了鎖定位的值,因此我們重新啟動循環而不是打印。

應該使用哪些 memory 命令來讀/寫 versioned_lock? 它應該是 memory_order_seq_cst 還是限制較少的東西?

認為以下就足夠了:

static constexpr uint64_t locked_bit = (1ULL << 63);

std::atomic<int> data1 = 0;
std::atomic<int> data2 = 0;
std::atomic<uint64_t> versioned_lock = 0;

int main() {
    std::thread t1([&]{
        restart:
        auto s = versioned_lock.load(std::memory_order_acquire); // L1
        if (s & locked_bit)
            goto restart;

        auto local_data1 = data1.load(std::memory_order_relaxed);
        auto local_data2 = data2.load(std::memory_order_relaxed);
        std::atomic_thread_fence(std::memory_order_acquire);    // AF
        if (s == versioned_lock.load(std::memory_order_relaxed)) { // L2
            // data has not been overwritten yet, can safely use local_data
            std::cout << local_data1 << " " << local_data2 << std::endl;
        } else {
            // possible race, local_data might be garbage?
        }
    });

    std::thread t2([&]{
        auto current_lock_version = versioned_lock.load(std::memory_order_relaxed);
        current_lock_version++;
        versioned_lock.store(current_lock_version | locked_bit, std::memory_order_relaxed); // S1
        std::atomic_thread_fence(std::memory_order_release); // RF
        data1.store(1234, std::memory_order_relaxed);
        data2.store(4321, std::memory_order_relaxed);
        versioned_lock.store(current_lock_version, std::memory_order_release); // S2
    });

    t1.join();
    t2.join();
}

也就是說,清除和測試鎖定位的對versioned_lock的訪問必須是釋放和獲取,數據的存儲必須在釋放柵欄之前,數據的加載必須在獲取柵欄之后。 直覺上,這應該可以防止數據訪問從保護它們的鎖訪問之間泄漏出去。 (您也可以釋放所有數據存儲,然后刪除釋放柵欄,但這會不必要地強大:我們不關心兩個數據存儲是否相互重新排序。如果我們有更多的數據元素。這同樣適用於使數據加載獲取的選項。)

為了證明這有效,請注意我們必須避免的兩個結果是1234 00 4321 所以假設加載data1返回1234data2返回4321的情況是等價的)。 由於這是由 t2 存儲的值,釋放柵欄 (RF) 與獲取柵欄 (AF) 同步。 現在 S1 排在 RF 之前,AF 排在 L2 之前,所以我們得出結論,S1 發生在 L2 之前。 因此 L2 不能返回 0; 它返回locked_bit1

如果 L1 返回與 L2 不同的值,那么我們將不會打印。 如果 L1 返回locked_bit我們也不會打印。 剩下的一種情況是L1和L2都返回1。這種情況下L1已經讀取了S2寫入的值,L1/S2是acquire/release,所以S2和L1是同步的。 數據存儲在 S2 之前排序,L1 在數據加載之前排序,因此兩個數據存儲都發生在兩個數據加載之前。 因此我們看到兩個新值,並打印1234 4321

如果 data1 和 data2 是非原子的(只是 int 或什至一些更復雜的數據類型),它也會工作嗎?

不,那根本行不通。 該算法中沒有任何內容可以防止data1, data2的存儲和加載發生沖突; 只是當它們發生沖突時,我們對版本鎖的測試告訴我們不要使用這些數據。 但如果它們不是原子的,那么沖突的存儲和加載將是一場數據競爭,無論我們是否使用數據都會導致未定義的行為。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM