C++定时器回调中互斥锁死锁问题求助
首先得帮你理清死锁的根源:你现在的代码里,定时器线程在mainLoop中持有std::unique_lock的情况下直接执行了timeoutHandler()回调,而回调里又调用了stop()——stop()会尝试获取同一个互斥锁的lock_guard。这就导致了:
- 主线程调用
resetCallback后进入internalQuit,加锁设置状态后调用thread.join(),阻塞等待定时器线程结束; - 定时器线程此时拿着互斥锁,卡在回调里的
stop()请求锁的步骤; - 两边互相等待:主线程等线程结束,线程等锁释放,死锁就这么发生了。
接下来给你几个可行的解决方案,按推荐程度排序:
方案一:执行回调前释放互斥锁(最优解)
核心思路是:让回调在无锁状态下执行,这样回调里调用任何需要加锁的公共方法时,都能顺利获取锁。
修改你的mainLoop逻辑,在执行timeoutHandler()之前主动解锁,执行完回调后再重新加锁,同时每次重新加锁后都检查退出状态(防止回调里已经触发了停止逻辑):
void mainLoop(Function &&timeoutHandler) { while(true) { std::unique_lock lock{mutex}; // 等待running状态或退出信号 sleepCv.wait(lock, [this]{ return running || quit; }); if(quit) break; // 等待初始定时到期 auto initDeadline = std::chrono::steady_clock::now() + initTime; // 如果wait_until返回false,说明定时到期(不是被唤醒) if(!sleepCv.wait_until(lock, initDeadline, [this]{ return !running || quit; })) { // 执行回调前解锁 lock.unlock(); timeoutHandler(); // 回调执行完重新加锁,检查是否需要退出 lock.lock(); if(quit) break; } if(!running || quit) break; // 处理间隔定时循环 while(true) { auto intervalDeadline = std::chrono::steady_clock::now() + intervalTime; if(!sleepCv.wait_until(lock, intervalDeadline, [this]{ return !running || quit; })) { lock.unlock(); timeoutHandler(); lock.lock(); if(quit) break; } if(!running || quit) break; } if(quit) break; } }
这样修改后,当回调里调用stop()时,互斥锁处于未被持有的状态,stop()能顺利加锁设置running=false并通知条件变量,定时器线程在回调结束后重新加锁,会立刻检测到退出信号并终止循环,主线程的join()也能正常完成,死锁问题就解决了。
方案二:使用递归互斥锁(不推荐,治标不治本)
如果你不想大改逻辑,可以把std::mutex换成std::recursive_mutex——它允许同一个线程多次获取锁。这样回调里的stop()请求锁时,因为是同一个线程持有锁,不会阻塞。
但这种方案只是绕过了死锁,并没有解决根本问题:锁的持有时间会被拉长(从定时逻辑一直到回调执行完),可能引发其他潜在的线程安全问题,而且递归锁的性能也比普通互斥锁稍差,所以只适合临时应急,不推荐作为长期解决方案。
方案三:优化锁的粒度(进阶)
你可以把需要保护的变量拆分,比如将running、quit这类状态变量改成std::atomic<bool>,减少互斥锁的使用范围。不过条件变量的wait必须和互斥锁配合,所以这种方案需要调整的逻辑较多,适合对性能要求极高的场景。
对你之前尝试的补充
你之前尝试用std::move转移线程的思路其实解决不了死锁问题,因为死锁的根源是线程持有锁时调用回调,和线程的所有权转移无关,所以这个方向是走不通的。
最后,修改完代码后你的测试用例就能正常运行了:回调里的stop()能顺利执行,定时器会停止,第二个断言也能通过。
备注:内容来源于stack exchange,提问作者Deedss

