Принудительно удалить слот в boost :: сигналы2 - PullRequest
12 голосов
/ 12 января 2010

Я обнаружил, что boost :: signal2 использует своего рода ленивое удаление подключенных слотов, что затрудняет использование соединений как нечто, управляющее временем жизни объектов. Я ищу способ принудительно удалить слоты при отключении. Любые идеи о том, как обойти эту проблему, разрабатывая мой код по-другому, также приветствуются!

Это мой сценарий: у меня есть класс Command, отвечающий за выполнение чего-то, что требует асинхронного времени и выглядит примерно так (упрощенно):

class ActualWorker {
public:
    boost::signals2<void ()> OnWorkComplete;
};

class Command : boost::enable_shared_from_this<Command> {
public:
    ...

    void Execute() {
        m_WorkerConnection = m_MyWorker.OnWorkDone.connect(boost::bind(&Command::Handle_OnWorkComplete, shared_from_this());

        // launch asynchronous work here and return
    }

    boost::signals2<void ()> OnComplete;

private:
    void Handle_OnWorkComplete() {
        // get a shared_ptr to ourselves to make sure that we live through
        // this function but don't keep ourselves alive if an exception occurs.
        shared_ptr<Command> me = shared_from_this();

        // Disconnect from the signal, ideally deleting the slot object
        m_WorkerConnection.disconnect();

        OnComplete();

        // the shared_ptr now goes out of scope, ideally deleting this
    }

    ActualWorker m_MyWorker;
    boost::signals2::connection m_WorkerConnection;
};

Класс вызывается примерно так:

...
boost::shared_ptr<Command> cmd(new Command);
cmd->OnComplete.connect( foo );
cmd->Execute();
// now go do something else, forget all about the cmd variable etcetera.

Класс Command поддерживает себя, передавая себе shared_ptr, который связан с сигналом ActualWorker с помощью boost :: bind.

Когда работник завершает работу, вызывается обработчик в Command. Теперь, поскольку я хотел бы, чтобы объект Command был уничтожен, я отключаюсь от сигнала, как видно из кода выше. Проблема заключается в том, что фактический объект слота не удаляется при отключении, он только помечается как недействительный, а затем удаляется позднее. Это, в свою очередь, зависит от того, будет ли сигнал снова срабатывать, чего в моем случае не происходит, что приводит к тому, что время прорезания никогда не истекает. Таким образом, объект boost :: bind никогда не выходит из области видимости, удерживая shared_ptr в моем объекте, который никогда не будет удален.

Я могу обойти это, связывая указатель this вместо shared_ptr, а затем поддерживая мой объект живым, используя член shared_ptr, который я затем освобождаю в функции обработчика, но это отчасти заставляет дизайн чувствовать себя немного слишком сложным. Есть ли способ заставить сигналы2 удалить слот при отключении? Или есть что-то еще, что я мог бы сделать, чтобы упростить дизайн?

Любые комментарии приветствуются!

Ответы [ 5 ]

4 голосов
/ 15 февраля 2010

boost::signals2 очищает слоты во время подключения / вызова.

Таким образом, если все слоты отключаются от сигнала, повторный вызов сигнала не вызовет ничего, но должен очистить слоты.

Чтобы ответить на ваш комментарий, да, повторный вызов сигнала небезопасен, если есть другие подключенные слоты, так как они будут вызваны снова. В этом случае я предлагаю вам пойти другим путем и подключить фиктивный слот, а затем отключить его при вызове вашего «настоящего» слота. Подключение другого слота очистит устаревшие соединения, поэтому ваш слот должен быть освобожден.

Просто убедитесь, что вы не держите никаких ссылок, которые необходимо освободить, в пустую ячейку, или вы вернулись туда, откуда начали.

2 голосов
/ 19 июня 2010

Это невероятно раздражающий аспект boost :: сигналов2.

Подход, который я выбрал для его разрешения, заключается в том, чтобы сохранить сигнал в scoped_ptr, и когда я хочу принудительно отключить все слоты, я удаляю сигнал. Это работает только в тех случаях, когда вы хотите принудительно отключить все подключения к сигналу.

1 голос
/ 13 апреля 2013

Я наткнулся на ту же проблему, и мне действительно не хватает какой-то явной очистки в API.

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

Мой первый обходной путь - обертка сигнала, которая немного подправляет код отключения:

template <typename Signature>
struct MySignal
{
  // ...

  template <typename Slot>
  void disconnect (Slot&& s)
  {
    mPrivate.disconnect (forward (s));
    // connect/disconnect dummy slot to force cleanup of s
    mPrivate.connect (&MySignal::foo);
    mPrivate.disconnect (&MySignal::foo);
  }

private:
  // dummy slot function with matching signature
  // ... foo (...)

private:
  ::boost::signals2::signal<Signature> mPrivate;
};

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

В отсутствие альтернатив я в итоге исправил оригинальный класс signal ( Edit: i на самом деле был бы признателен за встроенное решение. Этот патч был моим последним средством ). Мой патч содержит около 10 строк кода и добавляет общедоступный метод cleanup_connections() к signal. Мой обертка сигнала вызывает очистку в конце методов отключения. Этот подход решил мои проблемы, и до сих пор у меня не возникало проблем с производительностью.

Редактировать: Вот мой патч для буста 1.5.3

Index: signals2/detail/signal_template.hpp
===================================================================
--- signals2/detail/signal_template.hpp
+++ signals2/detail/signal_template.hpp
@@ -220,6 +220,15 @@
           typedef mpl::bool_<(is_convertible<T, group_type>::value)> is_group;
           do_disconnect(slot, is_group());
         }
+        void cleanup_connections () const
+        {
+          unique_lock<mutex_type> list_lock(_mutex);
+          if(_shared_state.unique() == false)
+          {
+            _shared_state.reset(new invocation_state(*_shared_state, _shared_state->connection_bodies()));
+          }
+          nolock_cleanup_connections_from(false, _shared_state->connection_bodies().begin());
+        }
         // emit signal
         result_type operator ()(BOOST_SIGNALS2_SIGNATURE_FULL_ARGS(BOOST_SIGNALS2_NUM_ARGS))
         {
@@ -690,6 +699,10 @@
       {
         (*_pimpl).disconnect(slot);
       }
+      void cleanup_connections ()
+      {
+        (*_pimpl).cleanup_connections();
+      }
       result_type operator ()(BOOST_SIGNALS2_SIGNATURE_FULL_ARGS(BOOST_SIGNALS2_NUM_ARGS))
       {
         return (*_pimpl)(BOOST_SIGNALS2_SIGNATURE_ARG_NAMES(BOOST_SIGNALS2_NUM_ARGS));
1 голос
/ 20 декабря 2010

Я закончил тем, что выполнил свою собственную (подмножество) реализацию сигнала, основное требование состояло в том, чтобы слот был разрушен при вызове connection :: disconnect ().сигнала, сохраняющего все слоты в карте, от указателя реализации слотов до shared_ptr для реализации слотов вместо списка / вектора, тем самым предоставляя быстрый доступ к отдельным слотам без необходимости повторения по всем слотам.Реализация слота в моем случае в основном является boost :: function.

Соединения имеют слабый_птр к внутреннему классу реализации для сигнала и слабый_птр к типу реализации слота, чтобы позволить сигналу выйти из области видимости ииспользовать указатель слота в качестве ключа в карте сигналов, а также указание, является ли соединение все еще активным (нельзя использовать необработанный указатель, поскольку он может потенциально использоваться повторно).

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

Карта защищена мьютексом, чтобы обеспечить многопоточное использование.Чтобы предотвратить взаимные блокировки, мьютекс не удерживается при вызове слотов, однако это означает, что слот может быть отключен от другого потока непосредственно перед вызовом сигналом.Это также имеет место в случае обычного boost :: signal2, и в обоих этих сценариях необходимо уметь обрабатывать обратный вызов из сигнала даже после его отключения.

Для упрощения кода, когда сигналуволен, я заставляю все слоты отключаться во время этого.Это отличается от boost :: signal2, который делает копию списка слотов перед вызовом их, чтобы обрабатывать разъединения / соединения во время срабатывания сигнала.

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

Для другихСценарии, я смог заменить использование сигнала только boost :: function (таким образом, требуя, чтобы могло быть только одно соединение) или просто придерживаясь обходного пути в вопросе, где сам слушатель управляет своим временем жизни.

1 голос
/ 18 января 2010

Является ли поведение более строгим с scoped_connection?

Итак, а не:

void Execute() {
    m_WorkerConnection = m_MyWorker.OnWorkDone.connect(boost::bind
        (&Command::Handle_OnWorkComplete, shared_from_this());

    // launch asynchronous work here and return
}

...

boost::signals2::connection m_WorkerConnection;

Вместо использования:

void Execute() {
    boost::signals2::scoped_connection m_WorkerConnection
        (m_MyWorker.OnWorkDone.connect(boost::bind
        (&Command::Handle_OnWorkComplete, shared_from_this()));

    // launch asynchronous work here and return
}   // connection falls out of scope

(скомпоновано из boost::signals2::connection)

Я не использовал никакой сигнализации, так что это скорее предположение, чем что-либо еще, но после Execute() вам не нужно будет disconnect(), так как scoped_connection обрабатывает его для вас. Это скорее «упрощает дизайн», чем фактически решает вашу проблему. Но это может означать, что вы можете Execute(), а затем сразу ~Command() (или delete shared_ptr).

Надеюсь, это поможет.

РЕДАКТИРОВАТЬ: И под Execute(), то сразу ~Command() Я, очевидно, имею в виду за пределами вашего объекта Command. Когда вы создадите Команду для ее выполнения, вы сможете сказать:

cmd->Execute();
delete cmd;

или аналогичный.

...