2012-04-04 5 views
4

Betrachten Sie den folgenden C++ 11-Code, wobei die Klasse B instanziiert und von mehreren Threads verwendet wird. Weil B einen geteilten Vektor ändert, muss ich den Zugriff darauf in der ctor- und Mitgliedsfunktion foo von B sperren. Um die Elementvariable id zu initialisieren, verwende ich einen Zähler, der eine atomare Variable ist, weil ich von mehreren Threads darauf zugreife.Thread-sichere Initialisierung der atomaren Variablen in 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; 
    }; 
} 

Leider enthält dieser Code eine Race-Bedingung und nicht funktioniert wie folgt aus (manchmal die Ctor und foo() nicht die gleiche ID verwenden). Wenn ich die Initialisierung von id an den Ctor Körper zu bewegen, die durch einen Mutex gesperrt ist, funktioniert es:

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

Können Sie mir bitte helfen, zu verstehen, warum das letztere Beispiel funktioniert (ist es, weil es nicht die gleiche Mutex nicht verwendet ?)? Gibt es eine sichere Möglichkeit, id in der Initialisierungsliste von B zu initialisieren, ohne es in den Körper des Ctor zu sperren? Meine Anforderungen sind, dass idconst sein muss und dass die Initialisierung von id in der Initialisierungsliste stattfindet.

+4

Können Sie den tatsächlichen Code, der das Problem verursacht, posten. Der Code, den Sie gestellt haben, macht keinen Sinn (zumindest in Ermangelung einer Definition von "A"). Zum Beispiel können Sie nicht einfach auf 'sharedResource [id]' zugreifen, ohne tatsächlich etwas getan zu haben, um 'sharedResource' so zu verändern, dass sie' id + 1' Elemente enthält. Und wenn 'A' keine Memberfunktion' push_back' enthält, sollte der Code nicht einmal kompiliert werden. –

+0

@JamesKanze warum sollte 'A' ein' push_back' Mitglied brauchen? Ich sehe nur einen '(const char *, size_t)' -Konstruktor und einen Move/Copy-Konstruktor in Benutzung. OP: Bitte machen Sie dies ein [SSCCE] (http://sscce.org) wenn möglich – je4d

Antwort

2

Erste Sperren checked, gibt es nach wie vor eine grundlegende Logik Problem in den entsandten Code. Sie verwenden ++ counter als id. Betrachten Sie die allererste Erstellung von B, in einem einzigen Thread. B wird id == 1 haben; Nach der push_back sharedResource, haben Sie sharedResource.size() == 1, und die einzige rechtliche Index für den Zugriff wird 0 sein.

Darüber hinaus gibt es eine klare Wettlaufbedingung im Code. Auch wenn Sie das oben genannte Problem beheben (id mit counter ++ Initialisieren), nehme die sowohl counter und sharedResource.size() derzeit 0 sind; Sie haben gerade initialisiert.Thread betritt man den Konstruktor von B, Inkrementen counter, so:

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

es dann durch Gewinde unterbrochen ist 2 (bevor es erfasst den Mutex), die auch counter (2) erhöht, und verwendet seine früheren Wert (1) als id. Nach den push_back in Thread 2, haben wir jedoch nur sharedResource.size() == 1, und der einzige Recht Index 0.

In der Praxis würde ich zwei separate Variablen vermeiden (counter und sharedResource.size()), die den gleichen Wert haben sollten. Von Erfahrung: zwei Dinge, die gleich sein sollten wird nicht — der einzige Zeit redundante Informationen verwendet werden soll, wenn es für Steuerung verwendet wird; an einem bestimmten Punkt haben Sie eine assert(id == sharedResource.size()) oder etwas ähnliches. Ich würde verwenden so etwas wie:

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

Oder wenn Sie id const machen wollen.

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

(Beachten Sie, dass dies erfordert zweimal den Mutex erwerben Alternativ Sie die zusätzlichen Informationen passieren könnte notwendig um die Aktualisierung zu vervollständigen sharedResource zu getNewId(), und es die ganze Arbeit machen lassen.)

1

Wenn ein Objekt initialisiert wird, sollte es einem einzelnen Thread gehören. Wenn es fertig ist, initialisiert zu werden, wird es geteilt.

Wenn es eine threadsichere Initialisierung gibt, bedeutet dies sicherzustellen, dass ein Objekt für andere Threads nicht zugänglich ist, bevor es initialisiert wird.

Natürlich können wir thread-sichere assignment einer atomaren Variable diskutieren. Die Zuweisung unterscheidet sich von der Initialisierung.

+0

In seinem Beispiel ist das Objekt, das initialisiert wird (vom Typ 'B'), nur von einem einzigen Thread aus erreichbar (ich nehme an). Sein Problem ist, dass der Konstruktor dieses Objekts globale Ressourcen verwendet. –

0

Sie befinden sich in der Unterkonstruktorliste, die den Vektor initialisiert. Dies ist nicht wirklich eine atomare Operation. In einem Multi-Thread-System könnten Sie also von zwei Threads gleichzeitig getroffen werden. Dies ändert, was ID ist. Willkommen bei Thread Sicherheit 101!

Durch Verschieben der Initialisierung in den Konstruktor, der von der Sperre umgeben ist, kann nur ein Thread auf den Vektor zugreifen und ihn setzen.

Der andere Weg, dies zu beheben, wäre, dies in ein Singleton-Muster zu verschieben. Aber dann bezahlen Sie jedes Mal, wenn Sie das Objekt bekommen, das Schloss.

Jetzt können Sie in Dinge wie doppelte :)

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

+0

Double Checked Locking ist ein klares Anti-Pattern; es ist extrem schwierig, richtig und nie wirklich notwendig zu werden. In seinem Fall, wenn er 'sharedResource' ein Singleton machen würde, könnte die statische' instance'Funktion die Sperre erhalten und ein 'std :: shared_ptr' zurückgeben, dessen" destructor "-Objekt es freigibt. (Dies sollte ein Standardmuster für veränderbare Singletons in einer Multithread-Umgebung sein.) –

Verwandte Themen