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
相关产品推荐
相关产品推荐

