【问题标题】:Mutual dependency of two objects两个对象的相互依赖
【发布时间】:2021-09-19 16:29:25
【问题描述】:

我经常遇到这样的情况:两个对象需要相互了解,并且我们有一个相互聚合式的依赖关系(例如,一个对象处理 websocket 连接,另一个处理 dbus连接,我们需要双向转发消息)。 UML 图应该是这样的:

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

int main() {
  TypeA a;
  TypeB b;

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

  // ...
}

我发现这里存在潜在的内存问题。当main() 返回时,首先b 被销毁,然后a。在这两个步骤之间,a 可能仍在另一个线程中运行并访问指向 b 的指针,此时该指针无效,导致 seg-fault。

我目前对该问题的解决方案是使用 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 中解决这个问题?

【问题讨论】:

  • 选择一个作为所有者,并在一个方向使用shared_ptr?如果 websocket 连接被客户端关闭,您是否希望 dbus 连接关闭,反之亦然?
  • 您可以将所需的对象作为参数传递,而不是作为成员:communicate(a, b);

标签: c++ dependencies aggregation weak-references weak-ptr


【解决方案1】:

你可以定义一个第三类 C,这样

  • C 知道 A 和 B
  • A 知道 C
  • B 知道 C

当 A 或 B 完成他们的工作时,他们会通知 C,如果可能的话,C 会将工作转发给其他班级。使用此方案,您可以将其扩展到更多类。

【讨论】:

  • 你仍然有双向关系(但现在有C)。
  • 我刚刚意识到你在这里描述的实际上是“中介模式”。正如 Jarod42 所指出的,它并没有解决相互依赖的问题。此外,我检查的所有示例实现都存在我描述的相同内存问题。但是,创建更明确的所有权关系仍然非常有用,尤其是在有两个以上对象时。为了解决内存问题,我认为我仍然需要使用弱指针。
【解决方案2】:

当两个对象都在各自的线程中运行时,您可以使用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 组成。但不同的值和动作类型也可以通过多态消息类型或functial standard libraryfunction 来传达。

演员的执行循环:

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 上查看。

【讨论】:

    【解决方案3】:

    首先,您应该确保依赖关系确实在对象之间,而不是在方法调用之间。也许对象不一定要相互持有指针,您可以将相关对象传递给被调用的方法。

    如果您确实有相互的对象依赖关系(有时是这种情况),请确定两个对象中的一个是否拥有另一个。当您尝试使用类对问题建模时,经常会发生这种情况:例如,Window拥有 RenderingContext,因为如果渲染上下文不存在窗口关闭/销毁。在这种情况下,拥有的类实际上应该只保存一个指向所有者的常规指针。

    有时,两个对象需要相互引用。在这种情况下,使用智能指针可能是您想要的。不过,在这种情况下,您不需要 两个 指针都是 std::weak_ptr,只需一个,因为一个弱指针就足以打破依赖循环。

    关于您的多线程问题,您可能需要查看delete this; 成语。 https://isocpp.org/wiki/faq/freestore-mgmt#delete-this

    【讨论】:

      【解决方案4】:

      我理解您的问题的方式是,两个类都在运行它们自己的内部线程,如果封闭的指针变得无效,它们会崩溃。

      虽然您在对象中有 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.
      }
      

      【讨论】:

        猜你喜欢
        • 2011-04-26
        • 1970-01-01
        • 1970-01-01
        • 2012-12-22
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-11-11
        相关资源
        最近更新 更多