两个对象的相互依赖

Mutual dependency of two objects

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

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

int main() {
  TypeA a;
  TypeB b;

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

  // ...
}

我发现这里可能存在内存问题。当main()returns时,先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 中解决这个问题?

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

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

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

如果您确实有相互的对象依赖性(有时是这种情况),请弄清楚两个对象中的一个是否 拥有 另一个。当您尝试使用 classes 对问题建模时,这种情况经常发生:例如,a Window class owns a RenderingContext ,因为如果 window 是 closed/destroyed,渲染上下文 不存在 。在这种情况下,所拥有的 class 实际上应该只是指向所有者的常规指针。

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

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

你可以定义第三个 class C 这样

  • C认识A和B
  • A认识C
  • B认识C

当 A 或 B 完成他们的工作时,他们通知 C,如果可能的话,C 然后将工作转发给另一个 class。使用此方案,您可以将其扩展到更多 classes.

当两个对象 运行 在它们自己的线程中时,您可以使用 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 for the ability to transport values of different types. But different values and action types could also be conveyd by polymorphic message types or the function of the 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 中所见,不需要任何指针,不需要在堆上声明任何内容,也没有循环引用。通道是隔离线程和线程上的对象 运行 的一个不错的解决方案。参与者可以通过线程安全的通道进行操作和通信。

我的完整示例可以在 my Github repo 上查看。