C++多线程回调注册系统异常排查:输出缺失与死锁问题求助
C++多线程回调注册系统问题排查与修复
问题现象
- 极少打印“Pushing callback with id”注册日志
- 程序偶尔挂起,推测由竞态条件导致死锁
- 需求目标:事件触发前允许注册回调,触发后按注册顺序执行所有回调(顺序无需严格保证)
原代码核心问题分析
1. 无保护的共享变量访问引发竞态
registerCallbackAndExecute函数中,if (!eventTriggered)的判断未加锁,而registerEvent中直接修改eventTriggered = true,两者之间存在竞态。如果主线程先执行registerEvent设置eventTriggered,注册线程会直接进入else分支执行回调,跳过注册流程,导致注册日志极少出现。
2. 条件变量与共享变量的同步错误
releasedCallback是跨线程共享变量,但修改和读取都未加锁,callbackCond.wait的条件判断依赖这个变量,会导致线程读到脏数据,等待逻辑失效。callbackExecuted的修改和读取分别由注册线程和主线程操作,但未被同一mutex保护,eventCond.wait的唤醒条件不可靠,可能导致主线程永久等待,引发死锁。- 两个独立mutex(
callbackMutex和eventMutex)的设计增加了同步复杂度,反而容易引发锁顺序问题。
3. 回调执行模型混乱
原设计让注册回调的线程等待被唤醒后执行回调,同时主线程等待执行完成,这种模型没必要,且放大了同步风险。
修复方案
- 使用单个全局mutex保护所有共享状态,避免竞态条件
- 调整事件触发逻辑:主线程触发事件后,直接遍历并执行所有已注册的回调
- 简化条件变量的使用,仅在需要等待注册完成的场景下使用(可选)
- 确保所有共享变量的访问都在锁的保护范围内
修复后的代码
#include <condition_variable> #include <functional> #include <iostream> #include <mutex> #include <queue> #include <thread> class CallbackRegistrar{ public: void registerCallback(std::function<void()> callback) { std::unique_lock<std::mutex> lock(mtx); if (!eventTriggered) { auto saved_id = callback_id; std::cout << "Pushing callback with id " << saved_id << std::endl; registeredCallbacks.push(std::make_pair(callback_id, callback)); ++callback_id; } else { // 事件已触发,直接执行回调 callback(); } } void triggerEvent() { std::unique_lock<std::mutex> lock(mtx); eventTriggered = true; // 复制回调队列到局部,避免持有锁执行回调(防止回调中再次注册引发死锁) std::queue<std::pair<unsigned, std::function<void()>>> callbacksCopy = registeredCallbacks; registeredCallbacks = {}; lock.unlock(); // 提前解锁,避免回调执行时持有锁 // 按注册顺序执行所有回调 while (!callbacksCopy.empty()) { auto& cb = callbacksCopy.front().second; cb(); callbacksCopy.pop(); } } private: std::queue<std::pair<unsigned, std::function<void()>>> registeredCallbacks; bool eventTriggered{false}; std::mutex mtx; // 单个全局mutex保护所有共享状态 unsigned callback_id{1}; }; int main() { CallbackRegistrar registrar; std::thread t1(&CallbackRegistrar::registerCallback, std::ref(registrar), []{std::cout << "First!\n";}); std::thread t2(&CallbackRegistrar::registerCallback, std::ref(registrar), []{std::cout << "Second!\n";}); // 等待线程完成注册(如果需要确保所有注册完成再触发,可保留;否则可提前触发) t1.join(); t2.join(); registrar.triggerEvent(); return 0; }
额外说明
- 修复后的代码将回调执行逻辑移到主线程,避免了跨线程等待的复杂同步
- 复制回调队列到局部后解锁,防止回调函数中再次调用
registerCallback时引发死锁 - 如果需要支持事件触发后仍允许注册并立即执行,现有逻辑已覆盖该场景
内容的提问来源于stack exchange,提问作者user6646922
相关产品推荐
相关产品推荐

