簡體   English   中英

std :: mutex與std :: recursive_mutex作為類成員

[英]std::mutex vs std::recursive_mutex as class member

我見過有人討厭recursive_mutex

http://www.zaval.org/resources/library/butenhof1.html

但是,當考慮如何實現一個線程安全的(互斥量保護的)類時,在我看來,很難證明每個應該被互斥量保護的方法都是互斥量保護的,並且互斥量最多只能被鎖定一次。

因此,對於面向對象的設計,應該將std::recursive_mutex為默認值,並且通常將std::mutex視為一種性能優化,除非僅在一個地方使用(僅保護一個資源)?

為了清楚起見,我在談論一種私有的非靜態互斥量。 因此,每個類實例只有一個互斥體。

在每種公共方法的開頭:

{
    std::scoped_lock<std::recursive_mutex> sl;

在大多數情況下,如果您認為需要遞歸互斥鎖,那么您的設計是錯誤的,因此絕對不應該將其作為默認設置。

對於具有單個互斥量保護數據成員的類,則該互斥量應在所有public成員函數中被鎖定,而所有private成員函數應假定該互斥體已被鎖定。

如果一個public成員函數需要調用另一個public成員函數,則將第二個成員一分為二:執行工作的private實現函數和僅鎖定互斥鎖並調用private成員的public成員函數。 然后,第一個成員函數還可以調用實現函數,而不必擔心遞歸鎖定。

例如

class X {
    std::mutex m;
    int data;
    int const max=50;

    void increment_data() {
        if (data >= max)
            throw std::runtime_error("too big");
        ++data;
    }
public:
    X():data(0){}
    int fetch_count() {
        std::lock_guard<std::mutex> guard(m);
        return data;
    }
    void increase_count() {
        std::lock_guard<std::mutex> guard(m);
        increment_data();
    } 
    int increase_count_and_return() {
        std::lock_guard<std::mutex> guard(m);
        increment_data();
        return data;
    } 
};

這當然是一個瑣碎的示例,但是increment_data功能在兩個公共成員函數之間共享,每個公共成員函數都鎖定互斥體。 在單線程代碼,它會內聯到increase_countincrease_count_and_return可以調用,但我們不能這樣做,在多線程代碼。

這只是良好設計原則的一種應用:公共成員函數負責鎖定互斥鎖,並將完成工作的職責委托給私有成員函數。

這樣做的好處是,僅當類處於一致狀態時, public成員函數才需要處理:互斥體被解鎖,一旦互斥體被鎖定,則所有不變量都將保持不變。 如果您彼此調用public成員函數,則它們必須處理互斥體已被鎖定且不變式不一定成立的情況。

這也意味着條件變量等待之類的事情將起作用:如果您將遞歸互斥鎖傳遞給條件變量,則(a)您需要使用std::condition_variable_any因為std::condition_variable無效,並且(b )僅釋放了一個級別的鎖,因此您可能仍會持有該鎖,從而導致死鎖,因為將觸發謂詞並執行通知的線程無法獲取該鎖。

我很難想到需要遞歸互斥的情況。

應該將std::recursive_mutex設為默認值,將std::mutex視為性能優化嗎?

不是,不是 使用非遞歸鎖的優點是只是一個性能優化,這意味着你的代碼是自我檢查葉級的原子操作真的是葉級,他們不叫別的東西,它使用的鎖。

在某種情況下,您會遇到以下情況:

  • 一個實現一些需要序列化的操作的函數,因此它將使用互斥量並執行該操作。
  • 另一個函數,它實現較大的序列化操作,並希望在保持較大操作的鎖的同時調用第一個函數來執行其中的一個步驟。

舉一個具體的例子,也許第一個函數從列表中自動刪除一個節點,而第二個函數從列表中自動刪除兩個節點(並且您永遠不希望另一個線程只看到兩個節點中的一個來查看列表。出來)。

為此,您不需要遞歸互斥體。 例如,您可以將第一個函數重構為一個公共函數,該公共函數獲取鎖定並調用一個私有函數,該私有函數“不安全地”執行該操作。 然后,第二個函數可以調用相同的私有函數。

但是,有時使用遞歸互斥鎖比較方便。 這種設計仍然存在一個問題: remove_two_nodes在類不變式不成立的時候調用remove_one_node (第二次調用它時,列表恰好處於我們不想公開的狀態)。 但是,假設我們知道remove_one_node不依賴於該不變式,這並不是設計中的致命錯誤,只是我們使規則比理想的復雜得多,“只要有任何公共函數,所有類不變式始終成立”。輸入”。

因此,該技巧偶爾會很有用,並且我不討厭遞歸互斥體達到本文所能達到的程度。 我沒有歷史知識可以爭辯說,將它們包含在Posix中的原因與文章所說的“證明互斥量屬性和線程擴展”不同。 我當然不認為它們是默認值。

我認為可以肯定地說,如果在您的設計中不確定是否需要遞歸鎖,那么您的設計是不完整的。 稍后您會后悔一個事實,那就是您正在編寫代碼,並且您不知道根本上不重要的事情,例如是否允許已經持有該鎖。 因此,請勿“以防萬一”放置遞歸鎖。

如果您知道需要一個,請使用一個。 如果您知道不需要一個鎖,那么使用非遞歸鎖不僅是一種優化,它還有助於強制執行設計約束。 使第二把鎖失效比使它成功並隱瞞您不小心做了設計中永遠不應該做的事情這一事實要有用。 但是,如果您按照自己的設計進行操作,並且永遠不會雙重鎖定互斥鎖,那么您將永遠不會發現它是否是遞歸的,因此遞歸互斥鎖不會直接有害。

這個類比可能會失敗,但是這是另一種看待它的方法。 想象一下,您可以在兩種指針之間進行選擇:一種在取消引用空指針時會使用堆棧跟蹤中止程序,而另一種會返回0 (或將其擴展為更多類型:其行為就像指針指向一個值一樣) -初始化的對象)。 非遞歸互斥鎖有點像正在中止的互斥鎖,而遞歸互斥鎖有點像返回0的互斥鎖。它們都有潛在的用途-人們有時會花一些時間來實現“安靜而不是-a”。 -value”值。 但是,如果您的代碼設計為從不取消引用空指針,則默認情況下 ,您不想使用默默允許發生這種情況的版本。

我不會直接討論互斥鎖與recursive_mutex的爭論,但我認為最好共享一個場景,其中recursive_mutex對設計絕對至關重要。

當使用Boost :: asio,Boost :: coroutine(也許還有NT Fibers之類的東西,盡管我不太熟悉它們)時,即使沒有重新進入的設計問題,互斥體也是遞歸的,這一點絕對必要。

究其原因是因為它的設計理念的協同程序為基礎的方法將暫停例行執行,隨后恢復它。 這意味着一個類的兩個頂級方法可能“在同一線程上同時被調用”而沒有進行任何子調用。

暫無
暫無

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

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