微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

两个对象的相互依赖

如何解决两个对象的相互依赖

经常遇到这样的情况:两个对象需要相互了解,并且我们有一个相互聚合风格的依赖(想象一下,例如,一个对象处理一个 websocket 连接,另一个处理一个 dbus连接,我们需要双向转发消息)。 UML 图看起来像这样:

UML class diagram of mutual aggregation style dependency

在 C++ 中创建这种依赖关系的一种简单方法是相互传递指针:

int main() {
  TypeA a;
  TypeB b;

  a.SetB(&b);
  b.SetA(&a);

  // ...
}

在这里看到一个潜在的内存问题。当 main() 返回时,首先 b 被销毁,然后是 a在这两个步骤之间,a 可能仍在另一个线程中运行并访问指向 b 的指针,此时该指针无效,从而导致段错误

我目前对这个问题的解决方案是使用 C++11 智能指针。 TypeATypeB 都将 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 组成。但是,多态消息类型或 functionfunctial 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 举报,一经查实,本站将立刻删除。