Consider the following C++11 code where class B is instantiated and used by multiple threads. Because B modifies a shared vector, I have to lock access to it in the ctor and member function foo of B. To initialize the member variable id I use a counter that is an atomic variable because I access it from multiple threads.
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;
};
}
Unfortunately, this code contains a race condition and does not work like this (sometimes the ctor and foo() do not use the same id). If I move the initialization of id to the ctor body which is locked by a mutex, it works:
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));
}
};
Can you please help me understanding why the latter example works (is it because it does not use the same mutex?)? Is there a safe way to initialize id in the initializer list of B without locking it in the body of the ctor? My requirements are that id must be const and that the initialization of id takes place in the initializer list.
First, there’s still a fundamental logic problem in the posted code.
You use
++ counterasid. Consider the very first creation ofB,in a single thread.
Bwill haveid == 1; after thepush_backofsharedResource, you will havesharedResource.size() == 1, and theonly legal index for accessing it will be
0.In addition, there’s a clear race condition in the code. Even if you
correct the above problem (initializing
idwithcounter ++), supposethat both
counterandsharedResource.size()are currently0;you’ve just initialized. Thread one enters the constructor of
B,increments
counter, so:It is then interrupted by thread 2 (before it acquires the mutex), which
also increments
counter(to 2), and uses its previous value (1) asid. After thepush_backin thread 2, however, we have onlysharedResource.size() == 1, and the only legal index is 0.In practice, I would avoid two separate variables (
counterandsharedResource.size()) which should have the same value. Fromexperience: two things that should be the same won’t be—the only
time redundant information should be used is when it is used for
control; i.e. at some point, you have an
assert( id ==, or something similar. I’d use something like:sharedResource.size() )
Or if you want to make
idconst:(Note that this requires acquiring the mutex twice. Alternatively, you
could pass the additional information necessary to complete updating
sharedResourcetogetNewId(), and have it do the whole job.)