variable - windows c++ mutex lock example




如何在C++中處理可移動類型中的互斥鎖? (4)

通過設計, std::mutex 既不可移動,也不可複制構造。 這意味著持有互斥量的 A 類將不會收到default-move-constructor。

我如何使 A 型以線程安全的方式移動?


使用互斥體和C ++移動語義是在線程之間安全有效地傳輸數據的絕佳方法。

想像一下一個“生產者”線程,該線程生產成批的字符串並將其提供給(一個或多個)消費者。 這些批處理可由包含(可能很大) std::vector<std::string> 對象的對象表示。 我們絕對希望將這些向量的內部狀態“移動”到它們的使用者中,而不必進行不必要的重複。

您只需將互斥鎖識別為對象的一部分,而不是對象狀態的一部分。 也就是說,您不想移動互斥量。

您需要哪種鎖定取決於算法或對象的通用性以及允許的使用範圍。

如果只從共享狀態的“生產者”對象移動到線程本地的“消費”對象,則可以只鎖定 對象移動 對象。

如果是更一般的設計,則需要同時鎖定兩者。 在這種情況下,您需要考慮死鎖。

如果這是一個潛在的問題,則使用 std::lock() 以無死鎖的方式獲取兩個互斥鎖的鎖。

http://en.cppreference.com/w/cpp/thread/lock

最後,您需要確保您了解移動語義。 回想一下,從對象移出的對象處於有效但未知的狀態。 不執行移動的線程很有可能有充分的理由嘗試在找到有效但未知的狀態時嘗試從對象訪問移動的對象。

同樣,我的生產者只是在敲擊弦樂,而消費者卻要承擔全部負擔。 在這種情況下,生產者每次嘗試將向量添加到向量時,都可能會發現向量非空或為空。

簡而言之,如果對從對象移動的潛在並發訪問等於一次寫操作,則可能沒問題。 如果等於讀取,請考慮為什麼可以讀取任意狀態。


讓我們從一些代碼開始:

class A
{
    using MutexType = std::mutex;
    using ReadLock = std::unique_lock<MutexType>;
    using WriteLock = std::unique_lock<MutexType>;

    mutable MutexType mut_;

    std::string field1_;
    std::string field2_;

public:
    ...

我在其中放置了一些暗示性的類型別名,我們在C ++ 11中不會真正利用它,但在C ++ 14中變得更加有用。 請耐心等待,我們會到達的。

您的問題可以歸結為:

如何為此類編寫move構造函數和move賦值運算符?

我們將從move構造函數開始。

移動構造函數

請注意,成員 mutexmutable 。 嚴格來說,這對於move成員來說不是必需的,但是我假設您也想要copy成員。 如果不是這種情況,則無需使互斥 mutable

構造 A ,不需要鎖定 this->mut_ 。 但是您確實需要鎖定 mut_ 其構造(移動或複制)對象的 mut_ 。 可以這樣完成:

    A(A&& a)
    {
        WriteLock rhs_lk(a.mut_);
        field1_ = std::move(a.field1_);
        field2_ = std::move(a.field2_);
    }

請注意,我們必須首先默認構造此成員,然後僅在 a.mut_ 鎖定後才為其分配值。

移動分配

移動分配運算符實際上要復雜得多,因為您不知道是否有其他線程正在訪問分配表達式的lhs或rhs。 通常,您需要注意以下情況:

// Thread 1
x = std::move(y);

// Thread 2
y = std::move(x);

這是正確保護以上情況的移動分配運算符:

    A& operator=(A&& a)
    {
        if (this != &a)
        {
            WriteLock lhs_lk(mut_, std::defer_lock);
            WriteLock rhs_lk(a.mut_, std::defer_lock);
            std::lock(lhs_lk, rhs_lk);
            field1_ = std::move(a.field1_);
            field2_ = std::move(a.field2_);
        }
        return *this;
    }

請注意,一個必須使用 std::lock(m1, m2) 來鎖定兩個互斥鎖,而不是一個接一個地鎖定它們。 如果一個接一個地鎖定它們,那麼當兩個線程按相反的順序分配兩個對象時,就會出現死鎖。 std::lock 的要點是避免死鎖。

複製構造函數

您沒有詢問複製成員,但是我們現在不妨討論它們(如果沒有,則有人需要它們)。

    A(const A& a)
    {
        ReadLock  rhs_lk(a.mut_);
        field1_ = a.field1_;
        field2_ = a.field2_;
    }

除了使用 ReadLock 別名代替 WriteLock 外,該複製構造函數與move構造函數非常相似。 當前,這兩個別名都是 std::unique_lock<std::mutex> ,因此並沒有什麼區別。

但是在C ++ 14中,您可以選擇這樣說:

    using MutexType = std::shared_timed_mutex;
    using ReadLock  = std::shared_lock<MutexType>;
    using WriteLock = std::unique_lock<MutexType>;

可能 是一種優化,但不是絕對的。 您將必須確定它是否存在。 但是通過這一更改,可以同時 多個線程中的同一rhs複製構造。 即使未修改rhs,C ++ 11解決方案也會迫使您將此類線程順序化。

複製作業

為了完整起見,這是副本賦值運算符,在閱讀了其他所有內容後,它應該很容易解釋:

    A& operator=(const A& a)
    {
        if (this != &a)
        {
            WriteLock lhs_lk(mut_, std::defer_lock);
            ReadLock  rhs_lk(a.mut_, std::defer_lock);
            std::lock(lhs_lk, rhs_lk);
            field1_ = a.field1_;
            field2_ = a.field2_;
        }
        return *this;
    }

等等。

如果您希望多個線程能夠一次調用它們,則訪問 A 狀態的任何其他成員或自由函數也將受到保護。 例如,這裡是 swap

    friend void swap(A& x, A& y)
    {
        if (&x != &y)
        {
            WriteLock lhs_lk(x.mut_, std::defer_lock);
            WriteLock rhs_lk(y.mut_, std::defer_lock);
            std::lock(lhs_lk, rhs_lk);
            using std::swap;
            swap(x.field1_, y.field1_);
            swap(x.field2_, y.field2_);
        }
    }

請注意,如果僅依靠 std::swap 來完成這項工作,則鎖定的粒度將錯誤, std::swap 將在內部執行的三步移動之間進行鎖定和解鎖。

的確,考慮 swap 可以使您深入了解可能需要提供“線程安全” A API,由於“鎖定粒度”,通常與“非線程安全” API有所不同。問題。

還要注意需要防止“自我交換”。 “自我交換”應該是禁止操作的。 沒有自檢,將遞歸鎖定同一互斥鎖。 也可以通過使用 MutexType std::recursive_mutex 進行自檢,從而解決此問題。

更新資料

在下面的評論中,Yakk對於必須默認在副本中構造事物並移動構造方法(他有一點要點)感到非常不滿。 如果您對這個問題有足夠的信心,以至於您願意花時間在它上面,那麼可以這樣避免:

  • 添加所需的任何鎖類型作為數據成員。 這些成員必須位於受保護的數據之前:

    mutable MutexType mut_;
    ReadLock  read_lock_;
    WriteLock write_lock_;
    // ... other data members ...
  • 然後在構造函數(例如,複製構造函數)中執行以下操作:

    A(const A& a)
        : read_lock_(a.mut_)
        , field1_(a.field1_)
        , field2_(a.field2_)
    {
        read_lock_.unlock();
    }

糟糕,Yakk在我有機會完成此更新之前刪除了他的評論。 但是他值得推崇,因為他推動了這個問題,並為這個答案找到了解決方案。

更新2

dyp提出了這個好建議:

    A(const A& a)
        : A(a, ReadLock(a.mut_))
    {}
private:
    A(const A& a, ReadLock rhs_lk)
        : field1_(a.field1_)
        , field2_(a.field2_)
    {}

鑑於似乎沒有一個很好的,乾淨的,簡單的方法來回答這個問題-我 認為 安東的解決方案是正確的,但肯定值得商,,除非出現更好的答案,否則我建議將此類放在堆上並照看它通過 std::unique_ptr

auto a = std::make_unique<A>();

它現在是完全可移動的類型,並且在發生移動時在內部互斥鎖上處於鎖定狀態的任何人仍然是安全的,即使它是否是一件好事也存在爭議

如果您需要復制語義,請使用

auto a2 = std::make_shared<A>();

首先,如果要移動包含互斥量的對象,則設計一定存在問題。

但是,如果您仍然決定這樣做,則必須在move構造函數中創建一個新的互斥體,例如:

// movable
struct B{};

class A {
    B b;
    std::mutex m;
public:
    A(A&& a)
        : b(std::move(a.b))
        // m is default-initialized.
    {
    }
};

這是線程安全的,因為move構造函數可以安全地假定其參數未在其他任何地方使用,因此不需要鎖定參數。





move-constructor