假设我们有类似于以下伪代码的内容,其目标是实现并发并利用 RAII:
class Foo {
public:
vector<int> nums;
mutex lock;
};
class Bar {
public:
Bar(Foo &foo) : m_foo(foo)
{
lock_guard<mutex>(foo.lock);
m_num = foo.nums.back();
foo.nums.pop_back();
}
~Bar()
{
lock_guard<mutex>(foo.lock);
foo.nums.push_back(m_num);
}
private:
Foo &m_foo;
int m_num;
};
然后,假设我们可能有任意数量的 Bar 实例,其想法是,当它们超出范围时,析构函数会将其持有的“资源”返回给 Controller Foo 类。然而,我们还需要确保线程安全,因此需要锁。然而,我对这种设计有点警惕,因为直观上在析构函数中使用互斥体似乎是一个坏主意。我是否想得太多了,或者如果不是,是否有更好的方法来利用 RAII?
最佳答案
在析构函数中锁定互斥锁本质上没有任何问题。例如,共享资源可能需要成为线程安全的。那么,释放共享资源的所有权可能需要锁定互斥体。如果 RAII 只是在多线程编程中崩溃,那么它就不会是一个非常有用的工具。确实,访问了std::shared_ptr
的控制 block 是线程安全的,包括在共享指针销毁期间递减引用计数器时。显然,这通常是使用原子操作而不是互斥锁来实现的(不要引用我的话),但上下文是相同的:您在销毁期间释放共享资源的所有权,并且必须将其记录在线程安全的方式。
但是,请记住:锁定互斥体可能会引发异常,并且您应该(几乎)始终使用 try/catch 在析构函数中吸收异常。否则,如果堆栈已经由于另一个异常而展开,则程序将立即且不可恢复地终止,无论调用代码是否具备吸收原始异常和/或析构函数异常的能力。
但是可能有一种方法可以重组您的代码以完全避免该问题:A Bar
实际上并不需要对 Foo
的引用;它只需要一个 int
。在您的代码中,Bar
请求 int
从给定的Foo
。当 Bar
被破坏了,需要给int
返回Foo
以便可以回收利用;这需要存储对 Foo
的内部引用在其整个生命周期中,以便在破坏期间可以与其通信。 相反,请考虑给出 int
到Bar
直接在施工后,并采取int
毁灭后远离它。这就是 dependency injection 背后的驱动原理,构成“SOILD”中的“D”。因此,它带来了依赖注入(inject)的所有典型优点(例如,提高 Bar
类的可测试性)。
例如,可以在管理 Foo
的更大类中跟踪此逻辑。对象及其所有关联的 Bar
对象。这是一些伪代码,但确切的接口(interface)详细信息将取决于您的应用程序:
class BarPool:
Foo foo;
Map<int, Bar> bars;
mutex m;
BarPool(Foo foo) : foo(foo) {}
int add_bar():
lock m;
// Note: foo.pop() should probably be made thread-safe
// by internally locking / unlocking foo's mutex
int i = foo.pop()
bars.add(i, new Bar(i));
unlock m;
return i
void remove_bar(int i):
lock m;
// foo.push() should also probably be made thread-safe
bars.remove(i)
foo.push(i)
unlock m;
...
关于c++ - 在析构函数中获取锁是一个坏主意吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/74008646/