You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

boost::signals2::scoped_connection多线程销毁线程安全问题求解

Boost.Signals2 多线程下对象销毁与槽回调的线程安全问题

问题背景

有如下类结构,使用boost::signals2::scoped_connection关联信号与槽,槽触发时输出类的成员变量:

struct A {
   A(boost::signals2::signal<void()>& connection)
    : scoped_connection_{connection.connect([this]() {
          std::cout << foo_ << std::endl;
      })} {}

    std::string foo_{"foo"};
    boost::signals2::scoped_connection scoped_connection_;
};

但在多线程场景中,若一个线程触发信号执行槽回调,同时另一个线程销毁对象,会直接触发段错误:

using Signal = boost::signals2::signal<void()>;

struct A {
    A(Signal& connection)
        : scoped_connection_{connection.connect([this]() {
              std::this_thread::sleep_for(std::chrono::seconds(5));
              // foo_已被销毁,访问导致段错误
              std::cout << foo_ << std::endl;
          })} {}

    std::string foo_{"Kaboom"};
    boost::signals2::scoped_connection scoped_connection_;
};

int main() {
    Signal sig;
    auto a = std::make_unique<A>(sig);

    std::thread t([&sig]() { sig(); });

    // 等待线程启动并触发槽
    std::this_thread::sleep_for(std::chrono::seconds(1));

    // 回调执行中销毁对象
    a.reset();
   
    t.join();
    return 0;
}

崩溃原因在于:scoped_connection的析构函数不会阻塞等待正在运行的槽回调完成,即使回调还在执行,对象仍会被销毁,导致回调访问已释放的内存。

可行解决方案

方案1:共享所有权(std::shared_ptr + std::weak_ptr)

利用std::weak_ptr的特性,在槽回调中先尝试锁定对象,确认对象未被销毁后再访问成员。需要让类继承std::enable_shared_from_this:

using Signal = boost::signals2::signal<void()>;

struct A : std::enable_shared_from_this<A> {
    A(Signal& sig) {
        scoped_connection_ = sig.connect([weak_self = weak_from_this()]() {
            // 尝试锁定对象,失败则说明已销毁,直接返回
            if (auto self = weak_self.lock()) {
                std::this_thread::sleep_for(std::chrono::seconds(5));
                std::cout << self->foo_ << std::endl;
            }
        });
    }

    std::string foo_{"Kaboom"};
    boost::signals2::scoped_connection scoped_connection_;
};

int main() {
    Signal sig;
    auto a = std::make_shared<A>(sig);

    std::thread t([&sig]() { sig(); });
    std::this_thread::sleep_for(std::chrono::seconds(1));
    a.reset();
   
    t.join();
    return 0;
}

这种方式下,对象销毁后回调会自动跳过后续逻辑,避免非法内存访问。

方案2:同步等待回调执行完成

在类中添加同步原语,析构时等待槽回调执行完毕:

using Signal = boost::signals2::signal<void()>;

struct A {
    A(Signal& sig)
        : scoped_connection_{sig.connect([this]() {
              std::lock_guard<std::mutex> lock(mtx_);
              is_running_ = true;
              lock.unlock();

              std::this_thread::sleep_for(std::chrono::seconds(5));
              
              std::lock_guard<std::mutex> lock2(mtx_);
              std::cout << foo_ << std::endl;
              is_running_ = false;
              cv_.notify_one();
          })} {}

    ~A() {
        std::unique_lock<std::mutex> lock(mtx_);
        // 等待回调执行完成
        cv_.wait(lock, [this]() { return !is_running_; });
    }

    std::string foo_{"Kaboom"};
    boost::signals2::scoped_connection scoped_connection_;
    std::mutex mtx_;
    std::condition_variable cv_;
    bool is_running_ = false;
};

int main() {
    Signal sig;
    auto a = std::make_unique<A>(sig);

    std::thread t([&sig]() { sig(); });
    std::this_thread::sleep_for(std::chrono::seconds(1));
    a.reset();
   
    t.join();
    return 0;
}

该方案确保对象销毁前必须等待回调执行完毕,适合要求回调必须完成的场景,注意要合理控制锁的范围,避免死锁。

方案3:boost::signals2::trackable(不推荐)

Boost.Signals2提供了trackable基类,当对象销毁时会自动断开信号连接,但该机制在多线程下并非完全安全,且Boost官方已不推荐使用,因此不建议采用。

总结

  • 若允许回调在对象销毁后自动终止,优先选择方案1,实现更灵活且无额外同步开销;
  • 若要求回调必须执行完成才能销毁对象,选择方案2,通过同步原语保证执行顺序。

内容的提问来源于stack exchange,提问作者Pablo Arias

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 12:58:20