簡體   English   中英

C++中原子變量的線程安全初始化

[英]Thread-safe initialization of atomic variable in C++

考慮以下 C++11 代碼,其中 class B被實例化並由多個線程使用。 因為B修改了共享向量,所以我必須在B的構造函數和成員 function foo 中鎖定對它的訪問。 為了初始化成員變量id ,我使用了一個計數器,它是一個原子變量,因為我從多個線程訪問它。

struct A {
  A(size_t id, std::string const& sig) : id{id}, signature{sig} {}
private:
  size_t id;
  std::string signature;
};
namespace N {
  std::atomic<size_t> counter{0};
  typedef std::vector<A> As;
  std::vector<As> sharedResource;
  std::mutex barrier;

  struct B {
    B() : id(++counter) {
      std::lock_guard<std::mutex> lock(barrier);
      sharedResource.push_back(As{});
      sharedResource[id].push_back(A("B()", id));
    }
    void foo() {
      std::lock_guard<std::mutex> lock(barrier);
      sharedResource[id].push_back(A("foo()", id));
    }
  private:
    const size_t id;
  };
}

不幸的是,這段代碼包含競爭條件並且不能像這樣工作(有時 ctor 和 foo() 不使用相同的 id)。 如果我將 id 的初始化移動到被互斥鎖鎖定的 ctor 主體,它會起作用:

struct B {
  B() {
    std::lock_guard<std::mutex> lock(barrier);
    id = ++counter; // counter does not have to be an atomic variable and id cannot be const anymore
    sharedResource.push_back(As{});
    sharedResource[id].push_back(A("B()", id));
  }
};

你能幫我理解為什么后一個例子有效嗎(是因為它沒有使用相同的互斥量嗎?)? 有沒有一種安全的方法可以在B的初始化列表中初始化id而無需將其鎖定在 ctor 的主體中? 我的要求是id必須是const並且id的初始化發生在初始化列表中。

首先,發布的代碼中仍然存在一個基本的邏輯問題。 您使用++ counter作為id 考慮在單個線程中首次創建B B將有id == 1 sharedResourcepush_back之后,您將擁有sharedResource.size() == 1 ,並且訪問它的唯一合法索引將是0

此外,代碼中存在明顯的競爭條件。 即使您更正了上述問題(使用counter ++初始化id ),假設countersharedResource.size()當前均為0 你剛剛初始化。 線程一進入B的構造函數,遞增counter ,因此:

counter == 1
sharedResource.size() == 0

然后它被線程 2 中斷(在它獲取互斥鎖之前),線程 2 也將counter遞增(到 2),並將其先前的值 (1) 用作id 然而,在線程 2 中的push_back之后,我們只有sharedResource.size() == 1 ,唯一合法的索引是 0。

實際上,我會避免兩個單獨的變量( countersharedResource.size() ),它們應該具有相同的值。 根據經驗:應該相同的兩件事不會相同——唯一應該使用冗余信息的時間是在將其用於控制時; 即在某些時候,您有一個assert( id == sharedResource.size() )或類似的東西。 我會使用類似的東西:

B::B()
{
    std::lock_guard<std::mutex> lock( barrier );
    id = sharedResource.size();
    sharedResource.push_back( As() );
    //  ...
}

或者如果你想使id常量:

struct B
{
    static int getNewId()
    {
        std::lock_guard<std::mutex> lock( barrier );
        int results = sharedResource.size();
        sharedResource.push_back( As() );
        return results;
    }

    B::B() : id( getNewId() )
    {
        std::lock_guard<std::mutex> lock( barrier );
        //  ...
    }
};

(請注意,這需要獲取互斥量兩次。或者,您可以將完成更新sharedResource所需的附加信息傳遞給getNewId() ,並讓它完成整個工作。)

初始化 object 時,它應該由單個線程擁有。 然后當它完成初始化時,它就被共享了。

如果存在線程安全初始化這樣的事情,則意味着確保 object 在初始化之前沒有變得可供其他線程訪問。

當然,我們可以討論原子變量的線程安全assignment 賦值不同於初始化。

您在初始化向量的子構造函數列表中。 這不是真正的原子操作。 所以在多線程系統中,你可能會同時被兩個線程擊中。 這正在改變 id 是什么。 歡迎來到線程安全 101!

將初始化移動到被鎖包圍的構造函數中,這樣只有一個線程可以訪問和設置向量。

解決此問題的另一種方法是將其移動到單例模式中。 但是,每次獲得 object 時,您都需要為鎖付費。

現在你可以進入雙重檢查鎖定之類的東西了:)

http://en.wikipedia.org/wiki/Double-checked_locking

暫無
暫無

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

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