[英]Optimistic reads and Locking STM (Software Transactional Memory) with C/C++
[英]Using optimistic locks in C++ and memory order
我想知道“樂觀鎖”(如此處所述: https://db.in.tum.de/~leis/papers/artsync.pdf )如何在 C++ 中用於非原子數據以及什么應使用 memory 命令。
我對上述論文的理解是,以下代碼將同步t1
和t2
,而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();
}
我的問題是:
t1
實際上會打印“0 0”或“1234 4321”嗎?versioned_lock
? 它應該是memory_order_seq_cst
還是限制較少的東西? 如果data1
和data2
是非原子的(只是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 0
和0 4321
。 所以假設加載data1
返回1234
( data2
返回4321
的情況是等價的)。 由於這是由 t2 存儲的值,釋放柵欄 (RF) 與獲取柵欄 (AF) 同步。 現在 S1 排在 RF 之前,AF 排在 L2 之前,所以我們得出結論,S1 發生在 L2 之前。 因此 L2 不能返回 0; 它返回locked_bit
或1
。
如果 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.