Reputation: 439
In the C++ Seasoning video by Sean Parent https://youtu.be/W2tWOdzgXHA at 33:41 when starting to talk about “no raw synchronization primitives”, he brings an example to show that with raw synchronization primitives we will get it wrong. The example is a bad copy on write class:
template <typename T>
class bad_cow {
struct object_t {
explicit object_t(const T& x) : data_m(x) { ++count_m; }
atomic<int> count_m;
T data_m;
};
object_t* object_m;
public:
explicit bad_cow(const T& x) : object_m(new object_t(x)) { }
~bad_cow() { if (0 == --object_m->count_m) delete object_m; }
bad_cow(const bad_cow& x) : object_m(x.object_m) { ++object_m->count_m; }
bad_cow& operator=(const T& x) {
if (object_m->count_m == 1) {
// label #2
object_m->data_m = x;
} else {
object_t* tmp = new object_t(x);
--object_m->count_m; // bug #1
// this solves bug #1:
// if (0 == --object_m->count_m) delete object_m;
object_m = tmp;
}
return *this;
}
};
He then asks the audience to find the bug, which is the bug #1 as he confirms.
But a more obvious bug I guess, is when some thread is about to proceed to execute a line of code that I have denoted with label #2, while all of a sudden, some other thread just destroys the object and the destructor is called, which deletes object_m
. So, the first thread will encounter a deleted memory location.
Am I right? I don’t seem so!
Upvotes: 2
Views: 134
Reputation: 114461
Your objection doesn't hold because *this
at that moment is pointing to the object and the count is 1. The counter cannot get to 0 unless someone is not playing this game correctly (but in that case anything can happen anyway).
Another similar objection could be that while you're assigning to *this
and the code being executed is inside the #2 branch another thread makes a copy of *this
; even if this second thread is just reading the pointed object may see it mutating suddenly because of the assignment. The problem in this case is that count
was 1 when entering the if
in the thread doing the mutation but increased immediately after.
This is also however a bad objection because this code handles concurrency to the pointed-to object (like for example std::shared_ptr
does) but you are not allowed to mutate and read a single instance of bad_cow
class from different threads. In other words a single instance of bad_cow
cannot be used from multiple threads if some of them are writers without adding synchronization. Distinct instances of bad_cow
pointing to the same storage are instead safe to be used from different threads (after the fix #1, of course).
Upvotes: 2
Reputation: 73041
some other thread just destroys the object and the destructor is called, which deletes object_m. So, the first thread will encounter a deleted memory location.
Am I right? I don’t seem so!
Assuming the rest of the program isn't buggy, that shouldn't happen, because each thread should have its own reference-count object referencing the data_m
object. Therefore, if thread B has a bad_cow
object that references the data-object, then thread A cannot (or at least should not) ever delete that object, because the count_m
field can never drop to zero as long as there remains at least one reference-count object pointing to it.
Of course, a buggy program might encounter the race condition you suggest -- for example, a thread might be holding only a raw pointer to the data-object, rather than a bad_cow
that increments its reference count; or a buggy thread might call delete
on the object explicitly rather than relying on the bad_cow
class to handle deletion properly.
Upvotes: 3