2012-04-04 85 views
4

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

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; 
    }; 
} 

不幸的是,這個代碼包含一個競爭條件和這樣不工作(有時在構造函數和Foo()不使用相同的ID)。如果我移動ID的初始化是由一個互斥鎖的構造函數體,它的工作原理:

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)); 
    } 
}; 

能否請你幫我理解爲什麼後者的例子作品(是不是因爲它不使用相同的互斥?)?有沒有一種安全的方式來初始化id的初始化程序列表B而不鎖定在ctor的主體中?我的要求是,id必須是const並且id的初始化發生在初始化程序列表中。

+4

你能發佈導致問題的實際代碼嗎?你提出的代碼沒有意義(至少在缺少'A'的定義的情況下)。例如,你不能簡單地訪問'sharedResource [id]',而實際上沒有做一些事情來調整'sharedResource'來包含'id + 1'元素。除非'A'包含成員函數'push_back',否則代碼甚至不應該編譯。 – 2012-04-04 18:51:43

+0

@JamesKanze爲什麼'A'需要'push_back'成員?我只看到一個'(const char *,size_t)'構造函數和一個正在使用的移動/拷貝構造函數。 OP:如果可能,請將其設爲[SSCCE](http://sscce.org) – je4d 2012-04-04 19:57:51

回答

2

首先,仍然在發佈代碼的基本邏輯問題。 您使用++ counter作爲id。考慮在單個線程的第一個創作的B, 。 B將有id == 1;的 sharedResourcepush_back後,你將有sharedResource.size() == 1,並訪問它的 唯一合法指數將0

此外,還有在代碼中明確的競爭條件。即使 糾正了上述問題(初始化idcounter ++),假設 ,countersharedResource.size()當前0; 你剛剛初始化。螺紋一個進入的B, 增量counter構造,所以:

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

然後它被螺紋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); 
     // ... 
    } 
}; 

(注意,這需要獲取互斥兩次或者,你 可以通過必要的附加信息完成更新 sharedResourcegetNewId(),並讓它完成整個工作。)

1

當一個對象被初始化時,它應該由一個線程擁有。然後當它被完成初始化時,它被共享。

如果存在線程安全初始化這樣的事情,這意味着確保對象在初始化之前不能被其他線程訪問。

當然,我們可以討論一個原子變量的線程安全assignment。分配與初始化不同。

+0

在他的例子中,被初始化的對象(類型'B')只能從單個線程中訪問(我猜想)。他的問題是該對象的構造函數使用全局資源。 – 2012-04-05 07:51:48

0

您正在初始化矢量的子構造函數列表中。這不是一個真正的原子操作。所以在多線程系統中,您可能會同時碰到兩個線程。這正在改變什麼是身份證。歡迎來到線程安全101!

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

解決這個問題的另一種方法是將其移入singelton模式。但是,每次你拿到物品時你都在爲這個鎖付錢。

現在你可以進入東西像雙重檢查鎖定:)

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

+0

雙重鎖定是一種清晰的反模式;要獲得正確的,並且從來沒有真正必要的是非常困難的。在他的情況下,如果他將'sharedResource'設置爲一個單例,那麼靜態'instance'函數可以獲得該鎖,並返回其「析構函數」對象釋放它的'std :: shared_ptr'。 (這應該是多線程環境中可變單例的標準模式。) – 2012-04-05 07:56:02