Взаимная зависимость двух объектов

#c #dependencies #aggregation #weak-references #weak-ptr

Вопрос:

Довольно часто я сталкиваюсь с подобной ситуацией: два объекта должны знать друг друга, и у нас есть зависимость в стиле взаимной агрегации (представьте, например, что один объект обрабатывает соединение websocket, а другой обрабатывает соединение dbus, и нам нужно пересылать сообщения в обоих направлениях). Диаграмма UML будет выглядеть так:

UML-диаграмма классов зависимости от стиля взаимной агрегации

Простым способом создания этой зависимости в C было бы просто передавать указатели друг другу:

 int main() {
  TypeA a;
  TypeB b;

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

  // ...
}
 

Я вижу здесь потенциальную проблему с памятью. Когда main() возвращается, сначала b уничтожается, потом a . Между этими двумя шагами, a возможно , все еще выполняется в другом потоке и получает доступ к указателю на b , который в настоящее время недействителен, что вызывает ошибку seg.

Мое текущее решение этой проблемы-использование интеллектуальных указателей 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?

Комментарии:

1. Выберите один из них в качестве владельца и используйте shared_ptr в одном направлении? Вы хотите, чтобы соединение dbus закрывалось, если соединение websocket закрывается клиентом, или наоборот?

2. Вместо того, чтобы быть участником, вы можете передать необходимый объект в качестве параметров: communicate(a, b); .

Ответ №1:

Вы могли бы определить третий класс C таким образом, чтобы

  • C знает A и B
  • А знает С
  • Б знает С

Когда A или B выполняют свою работу, они сообщают об этом C, который затем переадресует задание другому классу, если это возможно. С помощью этой схемы вы можете расширить ее на большее количество классов.

Комментарии:

1. У вас все еще есть двунаправленная связь (но с C сейчас).

2. Мне только что сообщили, что то, что вы здесь описываете, на самом деле является «моделью посредника». Это не решает проблему взаимной зависимости, как отметил Джарод 42. Кроме того, все примеры реализаций, которые я проверил, страдают от той же проблемы с памятью, которую я описал. Тем не менее, по-прежнему очень полезно создать более явное отношение собственности, особенно когда существует более двух объектов. Чтобы решить проблему с памятью, я думаю, что мне все еще нужно прибегнуть к слабым указателям.

Ответ №2:

Когда оба объекта выполняются в своем собственном потоке, вы можете использовать каналы (или каналы, или очереди, или как вы хотите их называть) для связи между этими двумя объектами.

Вы создаете канал для каждого объекта и передаете ссылки на них в качестве отправляющего и принимающего концов соответственно объектам. Объекты, которые могут прослушивать на своей принимающей стороне сообщения в качестве действующих лиц и действовать в соответствии с новыми сообщениями, когда они получают и принимают их. Это разрушает циклическую зависимость, поскольку каждый объект теперь содержит ссылки на каналы, по которым они взаимодействуют друг с другом.

Канал:

 // The interface for the sending end of a Channel
template<typename T>
class SendingChannel {
  public:
    virtual void send(T) = 0;

    virtual ~SendingChannel() = defau<
};


// The interface for the receiving end of a Channel
template<typename T>
class ReceivingChannel {
  public:
    virtual T receive() = 0;

    virtual ~ReceivingChannel() = defau<
};


// 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;
    }
};
 

Сообщение, используемое для связи между объектами, может состоять из перечисления для обозначения типа и, возможно, варианта для возможности передачи значений разных типов. Но различные значения и типы действий также могут передаваться полиморфными типами сообщений или функцией функциональной стандартной библиотеки.

Цикл выполнения актера:

 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 , нет необходимости в каких-либо указателях, ничего не нужно объявлять в куче и нет циклических ссылок. Каналы-это достойное решение для изоляции потоков и объектов, работающих на них. Актеры могут действовать и общаться по потокобезопасным каналам.

Мой полный пример можно посмотреть в моем репозитории Github.

Ответ №3:

Во-первых, вы должны убедиться, что зависимость действительно существует между объектами, а не между вызовами методов. Возможно, объекты не обязательно должны содержать указатели друг на друга, вы можете передать соответствующий объект вызываемому методу.

Если у вас есть взаимная объектная зависимость (как это иногда бывает), выясните, принадлежит ли один из двух объектов другому. Это может часто происходить, когда вы пытаетесь смоделировать проблему с помощью классов: например , Window классу принадлежит a RenderingContext , потому что контекст визуализации не может существовать, если окно закрыто/уничтожено. В этом случае класс owned действительно должен содержать обычный указатель на владельца.

Иногда два объекта должны будут ссылаться друг на друга. В этом случае использование интеллектуальных указателей может быть тем, что вам нужно. Однако в таком случае вам не нужно, чтобы оба указателя были 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*) методы, которые вы вызывали бы в деструкторах объектов. Таким образом, вы избавитесь от своих проблем с memroy.

 class TypeB;

class TypeA {
   public:
       TypeA() = defau<

       ~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
}
 

Что касается вашего второго вопроса. Вам нужно подумать о том, кому принадлежат указатели. Все, в чем вам нужно быть уверенным, — это в том, что их срок службы контролируется и они удаляются в нужный момент времени. Если у вас есть что-то, что заботится об этом, вы можете просто отказаться weak_ptr от этого и вместо этого передать необработанный указатель:

 int main() {
  TypeA a;
  TypeB b;

  a.SetB(amp;b);    // pass address of b
  b.SetA(amp;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.
}