如何实现侵入式链表版信号槽机制的线程安全?
侵入式链表实现Qt风格信号槽的线程安全解决方案
核心问题拆解
多线程场景下的冲突根源在于两个关键操作的并发:
- 信号触发时遍历侵入式链表,调用连接节点对应的槽函数
- 信号/槽实例销毁时,从链表中移除连接节点并销毁该节点
直接用互斥量保护链表无法解决“节点正在被调用时不能销毁,销毁时不能再被调用”的依赖冲突,需要结合原子操作、引用计数等机制来处理。
具体解决方案
1. 连接节点的引用计数+原子失效标记
给每个Node添加原子引用计数器和失效标记,确保节点在被调用期间不会被销毁,销毁操作会等待所有正在进行的调用完成:
- 初始化
ref_count为1(初始持有:信号/槽的链表) - 信号触发调用
Node::foo()时,先递增引用计数,再检查失效标记:若未失效则执行槽函数,执行完成后递减引用计数;若计数归零则直接销毁节点 - 信号/槽销毁时,先将节点从链表中移除,再设置失效标记并递减引用计数:若计数归零则直接销毁,否则等待后续调用完成后自动销毁
2. 链表遍历的快照优化
为了减少互斥量持有时间,避免阻塞链表的修改操作:
- 信号触发时,先在互斥量保护下将链表节点复制到临时快照数组
- 释放互斥量后遍历快照调用槽函数
- 快照中的节点通过引用计数保证在调用期间不会被销毁
3. 槽实例的生命周期防护
给槽类B添加原子销毁标记,在槽函数执行前做合法性检查:
- 槽实例销毁时先设置
is_destroyed为true B::bar()执行前先检查该标记,若已销毁则直接返回- 该标记需配合引用计数使用,确保正在执行的槽函数完成后再释放槽实例资源
4. 侵入式链表的互斥保护
给信号类A和槽类B的侵入式链表各自添加专属互斥量:
- 所有链表的添加、移除、快照生成操作必须在互斥量保护下进行
- 遍历链表调用槽函数时,尽量缩短互斥量持有时间(比如仅在生成快照时持有)
关键代码片段
#include <atomic> #include <mutex> #include <vector> // 简化的侵入式链表节点 class Node { public: std::atomic<int> ref_count{1}; std::atomic<bool> invalid{false}; class A* signal; class B* slot; void trigger() { // 检查是否已失效,避免无效调用 if (invalid.load(std::memory_order_acquire)) return; // 递增引用计数,确保当前调用期间节点不会被销毁 ref_count.fetch_add(1, std::memory_order_acq_rel); if (!invalid.load(std::memory_order_acquire)) { slot->bar(); } // 递减引用计数,检查是否可以销毁节点 if (ref_count.fetch_sub(1, std::memory_order_acq_rel) == 1) { delete this; } } void invalidate() { invalid.store(true, std::memory_order_release); // 初始引用计数为1(链表持有),移除后递减,若归零则销毁 if (ref_count.fetch_sub(1, std::memory_order_acq_rel) == 1) { delete this; } } }; class B { public: std::atomic<bool> is_destroyed{false}; std::mutex list_mutex; IntrusiveList<Node> connections; // 自定义侵入式链表 ~B() { is_destroyed.store(true, std::memory_order_release); std::lock_guard<std::mutex> lock(list_mutex); while (!connections.empty()) { Node* node = connections.front(); connections.pop_front(); // 从信号的链表中移除该节点(信号类需提供互斥保护的移除方法) std::lock_guard<std::mutex> sig_lock(node->signal->list_mutex); node->signal->connections.remove(node); node->invalidate(); } } void bar() { if (is_destroyed.load(std::memory_order_acquire)) return; // 槽函数业务逻辑 } }; class A { public: std::mutex list_mutex; IntrusiveList<Node> connections; void emit_signal() { std::vector<Node*> snapshot; { std::lock_guard<std::mutex> lock(list_mutex); // 生成链表快照 for (auto* node : connections) { snapshot.push_back(node); } } // 遍历快照触发槽函数,无需持有互斥量 for (auto* node : snapshot) { node->trigger(); } } };
内容的提问来源于stack exchange,提问作者Patrick Wright
相关产品推荐
相关产品推荐

