这个设计问题一再出现,我仍然没有一个很好的解决方案。它可能会变成一种设计模式;)但是,它似乎非常特定于C ++(缺少垃圾收集)。无论如何,这是问题所在:
我们有一个父对象,它保留对子对象的引用。父母的州取决于其子女的州(某些总和)。为了通知其子女的状态变化,它会向他们传递对自己的引用。 (在另一个变体中,它向它们传递一个回调,子进程可以调用它来通知父进程。这个回调是一个闭包,它保持对父进程的引用。)应用程序是多线程的。现在,这个设置是整个大黄蜂的潜在竞争条件和死锁的巢。要理解原因,这是一个天真的实现:
class Parent {
public:
Parent() {
children_["apple"].reset(new Child("apple", this));
children_["peach"].reset(new Child("peach", this));
}
~Parent() {
}
void ChildDone(const string& child) {
cout << "Child is DONE: " << child << endl;
}
private:
map<string, linked_ptr<Child> > children;
};
class Child {
public:
Child(const string& name, Parent* parent)
: name_(name), parent_(parent), done_(false) {}
Foo(int guess) {
if (guess == 42) done_ = true;
parent->ChildDone(name_);
}
private:
const string name_;
Parent* parent_;
bool done_;
};
潜在问题:
我只是触及了表面,但人们可以想到其他潜在的问题。
我正在寻找的是关于如何在线程,锁定和动态添加/删除子项时处理父级的干净破坏的一些建议。如果有人提出了一个在多线程部署下强大的优雅解决方案,请分享。这里的关键字是健壮的:很容易设计一个带有一些巨大警告的结构(孩子从不调用父,父从不调用子,没有单独的线程用于回调等),挑战在于尽可能少地限制程序员。
答案 0 :(得分:3)
多线程问题的很大一部分通常是无法正确分离处理(工作线程,即Child)和状态。 锁定应该通过线程安全的数据结构而不是线程本身来完成。 消息队列,状态机和其他此类工具旨在允许您以受控方式管理此类数据,该方式独立于用于更新它们的过程。 您几乎总能重做这样的生命周期管理问题,以便它成为(线程安全的)数据管理问题。父可以被认为是状态的所有者,并且所有线程都更新状态本身。 用于管理对象生命周期的引用计数也是一种常见的范例。
答案 1 :(得分:1)
如果父母都有锁 和孩子们(非常可能是一个孩子 多线程非平凡 应用程序),锁定顺序成为一个 问题:父调用方法 孩子,反过来经历一个 状态转换并尝试通知 父母:死锁。
我不清楚为什么通知父母会导致死锁,除非
那是很多ifs。这是一个自然有问题的设计:一个线程(A)持有一个锁,并等待另一个线程(B)做某事。
没有神奇的解决方案来避免这个问题 - 你只需要避免它。最好的答案可能是不从单独的线程发回父母的信号;或者,用于区分已经或将不会被已经持有的父锁调用的信号。
在破坏父母的过程中,它 必须注意持续进行 来自其子女的回调。 特别是如果那些回调是 在一个单独的线程中被解雇了。如果它 不是,它可能已经消失了 调用回调。
这里的诀窍可能是孩子们应该有一个方法(也许是析构函数),这个方法在它返回后,孩子将不再进行回调。当父节点被销毁时,它会为每个子节点调用该方法。
我知道你要求“尽可能少的限制”,但实际上,在多线程环境中工作时,你必须有规则来防止死锁和竞争。
答案 2 :(得分:1)
设置一个标志以通知ChildDone
函数正在删除该对象,并在从析构函数返回之前等待任何正在运行的客户端线程完成。这可确保在线程执行ChildDone
时对象不会变为无效,并且在调用析构函数后不再接受对该函数的进一步调用。 (另见Should destructors be threadsafe?)。
// Pseudocode, not compilable C++.
class Parent {
// ....
~Parent() {
mutex_.acquire();
shuttingDown_ = true;
mutex_.release();
foreach (Child child in children_)
child->detachParent();
waitForRunningClientThreadToExit();
}
void ChildDone(const string& child) {
mutex_.acquire();
if (!shuttingDown_)
cout << "Child is DONE: " << child << endl;
mutex_.release();
}
bool volatile shuttingDown_ = false;
Mutex mutex_;
// ....
};
class Child {
// ...
Foo(int guess) {
if (guess == 42) done_ = true;
if (parent)
parent->ChildDone(name_);
}
void detachParent() {
parent = NULL;
}
};
答案 3 :(得分:0)
使用shared_ptr
,enable_shared_from_this
和weak_ptr
三重奏有一个解决方案。查看修改后的代码:
class Parent : public std::enable_shared_from_this<Parent> {
~Parent()
{
shuttingDown = true;
}
void addChild()
{
Child c{ "some_name", weak_from_this() };
c.foo();
}
void childDone(const std::string& child)
{
if (!shuttingDown)
std::cout << "Child is DONE: " << child << std::endl;
}
std::atomic_bool shuttingDown = false;
struct Child {
std::string name;
std::weak_ptr<Parent> parent;
void foo()
{
//do smth
if (auto shared_parent = parent.lock()) {
shared_parent->childDone(name);
}
}
};
};
此代码有效,但有一个严重的缺点-父对象无法在堆栈上分配,必须始终通过创建shared_ptr<Parent>
来创建它。 Here's a link to another question关于如何摆脱该限制。