如何解决两个对象的相互依赖
经常遇到这样的情况:两个对象需要相互了解,并且我们有一个相互聚合风格的依赖(想象一下,例如,一个对象处理一个 websocket 连接,另一个处理一个 dbus连接,我们需要双向转发消息)。 UML 图看起来像这样:
在 C++ 中创建这种依赖关系的一种简单方法是相互传递指针:
int main() {
TypeA a;
TypeB b;
a.SetB(&b);
b.SetA(&a);
// ...
}
我在这里看到一个潜在的内存问题。当 main()
返回时,首先 b
被销毁,然后是 a
。在这两个步骤之间,a
可能仍在另一个线程中运行并访问指向 b
的指针,此时该指针无效,从而导致段错误。
我目前对这个问题的解决方案是使用 C++11 智能指针。 TypeA
和 TypeB
都将 weak_ptr
存储到另一个,并且在访问它之前必须始终检查指针是否有效:
int main() {
auto a = std::make_shared<TypeA>();
auto b = std::make_shared<TypeB>();
a->SetB(b); // this method converts the shared_ptr to a weak_ptr
b->SetA(a); // this method converts the shared_ptr to a weak_ptr
// ...
}
我不确定这是否真的是一个合适的解决方案。另外,我不太高兴对象总是必须在堆上,我不能再把它们放在堆栈上。
谁能想象另一种解决方案?如何在 C++98 或 C 中解决这个问题?
解决方法
你可以定义第三个类 C 使得
- C 知道 A 和 B
- A 认识 C
- B 认识 C
当 A 或 B 完成他们的工作时,他们会通知 C,如果可能的话,C 会将工作转发给其他班级。使用此方案,您可以将其扩展到更多类。
,我理解您的问题的方式是,两个类都在运行自己的内部线程,如果封闭的指针无效,它们就会崩溃。
虽然对象中有 TypeA::setB(typeB*)
和 TypeB::setA(typeA*)
方法,但诀窍是同步 TypeA::deregisterB(typeB*)
和 TypeB::deregisterA(typeA*)
方法,您将在对象析构函数中调用它们.这样你就可以摆脱内存问题。
class TypeB;
class TypeA {
public:
TypeA() = default;
~TypeA() {
// stopThread
if (_b) {
_b->deregisterA(this);
}
}
void idle() {
...
}
void setB(TypeB* b) {
_b = b;
}
/**
* Disconnects _b from this.
* TypeB* : Object to deregister. A parameter is only required
* if TypeA has multiple pointers to TypeB.
*/
void derigisterB(TypeB* b) {
// ... wait for a save moment to delete b
_b = nullptr;
}
private:
TypeB* _b = nullptr;
};
class TypeB {
// ... same as TypeA
}
关于你的第二个问题。您需要考虑指针的所有权。您需要确定的是它们的生命周期受到控制,并且它们会在正确的时间点被删除。如果你有什么需要注意的,你可以放弃 die weak_ptr
并传递一个原始指针:
int main() {
TypeA a;
TypeB b;
a.SetB(&b); // pass address of b
b.SetA(&a); // pass address of a
// ...
// b will be deleted first. Its destructor calls a->deregisterB(this)
// method which sets a's pointer to b to nullptr.
// a will get deleted last. As it already knows there is no
// more b,it does not need to call deregisterA(this) on b.
}
,
首先,您应该确保依赖确实是在对象之间,而不是在方法调用之间。也许对象不一定要相互持有指针,您可以将相关对象传递给被调用的方法。
如果您确实有相互的对象依赖关系(有时是这种情况),请弄清楚两个对象中的一个是否拥有另一个。当您尝试使用类对问题建模时,这种情况经常发生:例如,Window
类拥有一个 RenderingContext
,因为渲染上下文不存在 em> 如果窗口关闭/毁坏。在这种情况下,拥有的类应该只保存一个指向所有者的常规指针。
有时,两个对象需要相互引用。在这种情况下,使用智能指针可能是您想要的。但是,在这种情况下,您不需要两个指针都是std::weak_ptr
,只需一个,因为一个弱指针就足以打破依赖循环。
关于您的多线程问题,您可能需要查看 delete this;
习语。
https://isocpp.org/wiki/faq/freestore-mgmt#delete-this
当两个对象都在自己的线程中运行时,您可以使用 channels(或管道、队列,或者您想如何调用它们)来进行这两个对象之间的通信。
您为每个对象创建一个通道,并将对它们的引用分别作为发送端和接收端传递给对象。对象可以作为参与者在其接收端侦听消息,并在接收和接受新消息时对新消息采取行动。这打破了循环依赖,因为每个对象现在都持有对它们相互通信的通道的引用。
频道:
// The interface for the sending end of a Channel
template<typename T>
class SendingChannel {
public:
virtual void send(T) = 0;
virtual ~SendingChannel() = default;
};
// The interface for the receiving end of a Channel
template<typename T>
class ReceivingChannel {
public:
virtual T receive() = 0;
virtual ~ReceivingChannel() = default;
};
// The implementation for a whole Channel
template<typename T>
class Channel: public SendingChannel<T>,public ReceivingChannel<T> {
private:
std::queue<T> msgs{};
std::mutex channel_mtx{};
std::condition_variable receiving_finishable{};
public:
bool is_empty() const { return msgs.empty(); }
bool is_not_empty() const { return !is_empty(); }
void send(T msg) override {
std::lock_guard channel_lck{channel_mtx};
msgs.push(std::move(msg));
receiving_finishable.notify_one();
}
T receive() override {
std::unique_lock channel_lck{channel_mtx};
receiving_finishable.wait(
channel_lck,[this](){ return is_not_empty(); }
);
T msg{std::move(msgs.front())};
msgs.pop();
return msg;
}
};
用于对象之间通信的消息可能由表示类型的枚举组成,也可能由用于传输不同类型值的能力的 variant 组成。但是,多态消息类型或 function 的 functial standard library 也可以传达不同的值和操作类型。
actor 的执行循环:
while (true) {
auto message = inbox.receive();
switch (message.type) {
case MsgType::PrintHello:
print_hello();
break;
case MsgType::PrintMessage:
print_message(get<std::string>(message.argument));
break;
case MsgType::GetValue:
send_value();
break;
case MsgType::Value:
print_value(get<int>(message.argument));
break;
}
}
主要:
int main() {
Channel<Message> to_b;
Channel<Message> to_a;
Object a("A",to_a,to_b);
Object b("B",to_b,to_a);
thread thread_a{a};
thread thread_b{b};
to_a.send(Message{MsgType::PrintMessage,"Hello,World!"});
to_b.send(Message{MsgType::PrintHello});
thread_a.join();
thread_b.join();
}
正如你在 main
中看到的,不需要任何指针,不需要在堆上声明任何东西,也没有循环引用。通道是隔离线程和在其上运行的对象的不错的解决方案。 Actor 可以通过线程安全的通道进行操作和通信。
可以在 my Github repo 上查看我的完整示例。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。