12

我发现 boost::signals2 使用了一种对连接槽的延迟删除,这使得很难将连接用作管理对象生命周期的东西。我正在寻找一种方法来强制在断开连接时直接删除插槽。任何关于如何通过不同地设计我的代码来解决问题的想法也值得赞赏!

这是我的场景:我有一个 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 类通过使用 boost::bind 将 shared_ptr 绑定到 ActualWorker 信号来使自己保持活动状态。

当工作人员完成时,将调用 Command 中的处理程序。现在,由于我希望销毁 Command 对象,因此我断开了与信号的连接,如上面的代码所示。问题是实际的槽对象在断开连接时并没有被删除,它只是被标记为无效,然后在以后删除。这反过来似乎取决于再次触发的信号,在我的情况下它没有这样做,导致插槽永不过期。boost::bind 对象因此永远不会超出范围,将 shared_ptr 保存到我的对象中,该对象永远不会被删除。

我可以通过使用 this 指针而不是 shared_ptr 进行绑定来解决此问题,然后使用成员 shared_ptr 保持我的对象处于活动状态,然后我在处理程序函数中释放该成员,但这有点让设计感觉有点过于复杂。有没有办法在断开连接时强制信号2删除插槽?或者我还能做些什么来简化设计?

任何意见表示赞赏!

4

5 回答 5

3

boost::signals2在连接/调用期间确实清理了插槽。

因此,如果所有插槽都与信号断开连接,第二次调用信号将不会调用任何东西,但它应该清理插槽。

要回答您的评论,是的,如果连接了其他插槽,则再次调用信号是不安全的,因为它们将被再次调用。在这种情况下,我建议您反过来连接一个虚拟插槽,然后在调用“真实”插槽时断开它。连接另一个插槽将清理陈旧的连接,因此您的插槽应该被释放。

只需确保您没有在虚拟插槽中保留任何需要释放的引用,否则您会回到开始的地方。

于 2010-02-15T12:48:32.127 回答
2

这是 boost::signals2 令人难以置信的烦人之处。

我解决它的方法是将信号存储在 scoped_ptr 中,当我想强制断开所有插槽时,我删除了信号。这仅适用于您想要强制断开与信号的所有连接的情况。

于 2010-06-19T02:37:31.580 回答
1

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

(从 a 复制构建boost::signals2::connection

我没有使用任何类型的信号,所以它比其他任何东西都更像是一个猜测,但是跟随Execute()你不需要disconnect(),因为 scoped_connection 会为你处理它。这更像是“简化设计”,而不是真正解决您的问题。但这可能意味着您可以Execute()然后立即~Command()(或deleteshared_ptr)。

希望有帮助。

编辑:到Execute()那时~Command()我显然是指从您的 Command 对象之外。当您构建命令来执行它时,您应该能够说:

cmd->Execute();
delete cmd;

或类似的。

于 2010-01-18T02:28:10.373 回答
1

我最终完成了自己的(子集)信号实现,主要要求是应该通过调用 connection::disconnect() 来销毁插槽。

该实现遵循将所有槽存储在映射中的信号线,从槽实现指针到用于槽实现而不是列表/向量的shared_ptr,从而快速访问各个槽,而无需遍历所有槽。在我的情况下,插槽实现基本上是一个 boost::function。

连接对信号的内部实现类有一个weak_ptr,对槽实现类型有一个weak_ptr,以允许信号超出范围并使用槽指针作为信号映射的键,以及指示是否连接仍然处于活动状态(不能使用原始指针,因为它可能会被重用)。

调用断开连接时,这两个弱指针都转换为 shared_ptrs ,如果这两个都成功,则要求信号实现断开指针给定的插槽。这是通过简单地从地图中删除它来完成的。

该映射受互斥体保护以允许多线程使用。为防止死锁,在调用插槽时不会保留互斥锁,但这意味着插槽可能会在被信号调用之前与不同的线程断开连接。这也是常规 boost::signals2 的情况,在这两种情况下,即使在断开连接后,也需要能够处理来自信号的回调。

为了简化触发信号时的代码,我强制在此期间断开所有插槽。这与 boost::signals2 不同,后者在调用它们之前会复制插槽列表,以便在触发信号时处理断开/连接。

以上方法适用于我的场景,其中感兴趣的信号很少被触发(在这种情况下只有一次),但是有很多短暂的连接,否则即使使用中概述的技巧也会占用大量内存这个问题。

对于其他情况,我已经能够仅用 boost::function 替换信号的使用(因此要求只能有一个连接),或者仅通过在侦听器本身管理的问题中坚持解决方法它的生命周期。

于 2010-12-20T14:58:54.797 回答
1

我偶然发现了同样的问题,我真的很想念 API 中的某种显式清理。

在我的场景中,我正在卸载一些插件 dll,并且我必须确保没有悬空对象(插槽)引用生活在卸载的 dll 中的代码(vftables 或其他任何东西)。由于延迟删除的东西,简单地断开插槽是行不通的。

我的第一个解决方法是一个信号包装器,它稍微调整了断开连接的代码:

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类(编辑:真的很感激内置解决方案。这个补丁是我最后的手段)。我的补丁大约有 10 行代码,并cleanup_connections()signal. 我的信号包装器在断开连接方法结束时调用清理。这种方法解决了我的问题,到目前为止我没有遇到任何性能问题。

编辑:这是我的 boost 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));
于 2013-04-13T13:17:47.087 回答