我有一个旧的C ++ 98代码库,它具有组合使用计数和pimpl设计,这种设计在多年前一直运行良好,直到4路和8路处理器变得普遍。在pimpl代码中你看到了这个(它实际上是真实代码中的模板):
class Foo {
FooImpl* impl;
public:
Foo() : impl(0) {}
Foo& operator=(const Foo& foo) {
if (foo.impl) foo.impl->addRef();
if (impl && impl->removeRef()) delete impl;
impl = foo.impl;
return *this;
}
// etc
}
问题出在多处理器环境中,任务切换可以在if (foo.impl)
和foo.impl->addRef()
之间进行。解决这个问题的第一个想法可能是:
Foo& operator=(const Foo& foo) {
FooImpl* fooimpl = foo.impl;
if (fooimpl) fooimpl->addRef();
if (impl && impl->removeRef()) delete impl;
impl = fooimpl;
return *this;
}
但这并没有解决问题。当您致电foo.impl
时,addRef()
仍然可能无效。是否有任何方法可以解决此问题并不涉及某种信号量?如果对象上的每个赋值运算符都包含在信号量中,那么性能将受到负面影响。
Foo& operator=(const Foo& foo) {
global_lock.acquire();
if (foo.impl) foo.impl->addRef();
if (impl && impl->removeRef()) delete impl;
impl = foo.impl;
global_lock.release();
return *this;
}
我怀疑答案是“不”。
答案 0 :(得分:3)
对于引用计数方案,需要设计和使用对象,以确保始终可以安全地添加引用。作为一个思想实验,为什么它应该是它的工作方式,考虑两个线程引用同一个对象。在这种情况下,永远不会出现其中一个线程遇到对象转换为0的引用计数的情况,因为它自己的引用应该防止这种情况发生。
在您描述的问题中,解决方案应该是将参考传递给其他任务的任务可以继续并且先发制人地添加引用。以下是一个说明这一点的方案:
class FooImpl {
friend class Foo;
unsigned count;
FooImpl() : count(1) {}
void addRef() { ++count; }
bool removeRef() { return !--count; }
};
class Foo {
FooImpl* impl;
void addRef() { if (impl) impl->addRef(); }
void removeRef() { if (impl && impl->removeRef()) delete impl; }
void swap(Foo &foo) {
FooImpl *tmp = impl; impl = foo.impl; foo.impl = tmp;
}
public:
~Foo() { removeRef(); }
Foo() : impl(0) {}
Foo(const Foo &foo) : impl(foo.impl) { addRef(); }
Foo& operator=(Foo foo) { swap(foo); return *this; }
// etc
};
注意,虽然多个线程可能引用相同的FooImpl
实例,但该方案要求每个线程都维护自己的Foo
。