[英]atomic exchange with memory_order_acquire and memory_order_release
[英]How C++ Standard prevents deadlock in spinlock mutex with memory_order_acquire and memory_order_release?
TL:DR:如果互斥鎖實現使用獲取和釋放操作,那么實現是否可以像通常允許的那樣進行編譯時重新排序並重疊兩個應該獨立的關鍵部分,來自不同的鎖? 這將導致潛在的僵局。
假設在std::atomic_flag
上實現了互斥鎖:
struct mutex
{
void lock()
{
while (lock.test_and_set(std::memory_order_acquire))
{
yield_execution();
}
}
void unlock()
{
lock.clear(std::memory_order_release);
}
std::atomic_flag lock; // = ATOMIC_FLAG_INIT in pre-C++20
};
到目前為止看起來還不錯,關於使用單個這樣的互斥鎖: std::memory_order_release
與std::memory_order_acquire
同步。
在這里使用std::memory_order_acquire
/ std::memory_order_release
不應該一見鍾情。 它們類似於 cppreference 示例https://en.cppreference.com/w/cpp/atomic/atomic_flag
現在有兩個互斥鎖保護不同的變量,兩個線程以不同的順序訪問它們:
mutex m1;
data v1;
mutex m2;
data v2;
void threadA()
{
m1.lock();
v1.use();
m1.unlock();
m2.lock();
v2.use();
m2.unlock();
}
void threadB()
{
m2.lock();
v2.use();
m2.unlock();
m1.lock();
v1.use();
m1.unlock();
}
釋放操作可以在不相關的獲取操作之后重新排序(不相關的操作 == 對不同對象的后續操作),因此可以將執行轉換如下:
mutex m1;
data v1;
mutex m2;
data v2;
void threadA()
{
m1.lock();
v1.use();
m2.lock();
m1.unlock();
v2.use();
m2.unlock();
}
void threadB()
{
m2.lock();
v2.use();
m1.lock();
m2.unlock();
v1.use();
m1.unlock();
}
所以看起來有一個僵局。
問題:
(不是C++11 memory_order_acquire 和 memory_order_release 語義的重復? ,雖然它在同一個區域)
ISO C++標准沒有問題; 它不區分編譯時和運行時重新排序,並且代碼仍然必須像在 C++ 抽象機上按源順序運行一樣執行。 所以m2.test_and_set(std::memory_order_acquire)
嘗試獲取第二個鎖的影響可以在仍然持有第一個鎖的同時對其他線程可見(即在m1.reset
之前),但是那里的失敗不能阻止m1
被釋放.
我們遇到問題的唯一方法是,如果編譯時重新排序將該命令確定為某些機器的 asm,這樣m2
鎖定重試循環必須在實際釋放m1
之前退出。
此外,ISO C++ 僅根據同步和可以看到的內容定義排序,而不是相對於某些新順序的重新排序操作。 這意味着存在某種秩序。 除非您使用 seq_cst 操作,否則甚至不能保證單獨的對象存在多個線程可以達成一致的這種順序。 (並且保證每個object單獨的修改訂單存在。)
獲取和釋放操作的 1-way-barrier model (如https://preshing.com/20120913/acquire-and-release-semantics中的圖表)是一種方便的思考方式,並且與現實相匹配。例如,在 x86 和 AArch64 上加載和純存儲。 但就語言律師而言,這不是 ISO C++ 標准定義事物的方式。
在長時間運行的循環中重新排序atomic
操作是 C++ 標准允許的理論問題。 P0062R1:編譯器何時應該優化原子? 指出標准的 1.10p28 措辭在技術上允許將存儲延遲到長時間運行的循環之后:
實現應確保由原子或同步操作分配的最后一個值(按修改順序)將在有限的時間段內對所有其他線程可見。
但是一個潛在的無限循環會違反這一點,例如在死鎖情況下不是有限的,所以編譯器不能這樣做。
這不僅僅是一個實施質量問題。 成功的互斥鎖是一個獲取操作,但您不應將重試循環視為單個獲取操作。 任何理智的編譯器都不會。
(激進的原子優化可能破壞的經典示例是進度條,其中編譯器將所有松弛存儲從循環中刪除,然后將所有死存儲折疊到一個 100% 的最終存儲中。另見此問答- 當前編譯器不會,並且基本上將atomic
視為volatile atomic
,直到 C++ 解決了為程序員提供一種方法讓編譯器知道何時可以/不能安全地優化 atomic 的問題。)
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.