在Sean Parent https://youtu.be/W2tWOdzgXHA的C ++ Seasoning视频中,在33:41开始谈论“没有原始同步原语”时,他举了一个例子来说明使用原始同步原语我们会弄错。该示例是写类的错误副本:
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;
}
};
然后,他要求观众找到错误,即他确认的错误#1。
但是我猜一个更明显的错误是,当某个线程将要执行执行我用标签#2表示的代码行时,突然之间,另一个线程只是破坏了对象和析构函数被称为,将删除object_m
。因此,第一个线程将遇到一个已删除的内存位置。
我是对的吗?我好像不是这样!
答案 0 :(得分:3)
其他一些线程只是破坏对象,而析构函数是 调用,删除object_m。因此,第一个线程将遇到一个 删除的内存位置。
我是对的吗?我好像不是这样!
假定程序的其余部分不是错误的,那应该不会发生,因为每个线程都应该有自己的引用计数对象,该对象引用data_m
对象。因此,如果线程B具有引用数据对象的bad_cow
对象,则线程A无法(或至少不应)删除该对象,因为count_m
字段永远不会降为零,因为只要至少有一个引用计数对象指向它。
当然,一个有缺陷的程序可能会遇到您建议的竞争条件-例如,一个线程可能仅持有指向数据对象的原始指针,而不是增加其引用计数的bad_cow
;否则,多虫线程可能会在对象上显式调用delete
,而不是依靠bad_cow
类来正确处理删除。
答案 1 :(得分:1)
您的异议并不成立,因为此时*this
指向该对象并且计数为1。除非有人没有正确玩此游戏,否则计数器不能为0(但在这种情况下,任何事物都可以还是发生)。
另一个类似的异议可能是,当您分配给*this
并且正在执行的代码在#2分支内时,另一个线程复制了*this
;即使第二个线程只是在读取指向的对象,也可能由于分配而使其突然发生变化。这种情况下的问题是,在进行突变的线程中输入count
时,if
为1,但之后立即增加。
但是,这也是一个不好的反对意见,因为此代码处理指向对象的并发(例如std::shared_ptr
这样),但是您不允许突变和读取bad_cow
的单个实例不同线程的类。换句话说,bad_cow
的单个实例不能在多个线程中使用,如果其中一些是编写者而不添加同步。可以安全地从不同线程使用bad_cow
指向同一存储的不同实例(当然,在修复#1之后)。