簡體   English   中英

C++ 標准如何使用 memory_order_acquire 和 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_releasestd::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();
}

所以看起來有一個僵局。

問題:

  1. 標准如何防止出現此類互斥鎖?
  2. 讓自旋鎖互斥體不受此問題困擾的最佳方法是什么?
  3. 這篇文章頂部的未修改互斥鎖是否可用於某些情況?

(不是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.

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