WaitableEvent 跨线程等待的死锁陷阱

场景还原

代码(Uninitialize 中的窗口销毁路径):

复制代码
if (workerWindow_) {
    if (content::GetUIThreadTaskRunner({})->RunsTasksInCurrentSequence()) {
        // 已在 UI 线程,直接销毁
        DestroyWindowOnUIThread();
    } else {
        // 不在 UI 线程,把销毁派发到 UI 线程,然后等待
        base::WaitableEvent window_destroyed(
            base::WaitableEvent::ResetPolicy::MANUAL,
            base::WaitableEvent::InitialState::NOT_SIGNALED);

        content::GetUIThreadTaskRunner({})->PostTask(FROM_HERE,
            base::BindOnce([](TaskbarHookManager* self, base::WaitableEvent* done) {
                self->DestroyWindowOnUIThread();
                done->Signal();
            }, base::Unretained(this), base::Unretained(&window_destroyed))
        );

        window_destroyed.Wait();  // ← 当前线程阻塞,等待 UI 线程执行完毕
    }
}

WaitableEvent::Wait() 的安全使用前提

WaitableEvent::Wait()裸阻塞,没有消息循环,调用时当前线程完全挂起,直到事件被 Signal。

安全条件

  1. 当前线程没有消息泵(不是 UI 线程、STA COM 线程、任何需要处理消息的线程)
  2. 等待的任务不依赖当前线程或当前线程上的任何资源(无循环依赖)
  3. 等待的任务保证一定会 Signal(无死路)

这里的潜在问题

情形 1:当前线程是 file 线程,UI 线程正常,看起来安全

复制代码
file 线程: window_destroyed.Wait()  ← 阻塞
                                          ↓
UI 线程:   DestroyWindowOnUIThread() → done->Signal()
                                          ↓
file 线程: 继续执行

表面上无死锁,但存在以下隐患:

隐患 A:UI 线程可能已经在等待 file 线程

如果 Uninitialize 本身是被 UI 线程发起的(CleanupHooksDCHECK_CALLED_ON_VALID_SEQUENCE),而 Uninitialize 的前半段已经在 file 线程上执行了某个阻塞性操作,就形成:

复制代码
UI 线程: 等待 file 线程完成清理(WaitableEvent::Wait)
              ↕
file 线程: 等待 UI 线程执行 DestroyWindow(WaitableEvent::Wait)
              → 死锁

隐患 B:TaskShutdownBehavior::SKIP_ON_SHUTDOWN

本项目的 file_task_runner_ 创建时使用了 SKIP_ON_SHUTDOWN

复制代码
file_task_runner_ = base::ThreadPool::CreateSequencedTaskRunner({
    base::MayBlock(),
    base::TaskPriority::USER_VISIBLE,
    base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN  // ← 关闭时跳过未执行的任务
});

在浏览器关闭流程中,如果 PostTask 到 UI 线程的任务因关闭而被丢弃(未执行),done->Signal() 永远不会被调用,window_destroyed.Wait() 永久阻塞,进程无法退出(或被系统强杀)。

隐患 C:UI 线程有消息泵但不是纯 Worker

DestroyWindow 本身会触发 WM_DESTROY 等消息,某些 DestroyWindowOnUIThread 实现可能向其他窗口发 SendMessage 进行同步通知,而那些窗口可能正在等待 file 线程 ------ 同样形成死锁。


WaitableEvent 正确 vs 错误用法对比

场景 推荐做法 原因
纯 Worker 线程等待另一个纯 Worker 线程 WaitableEvent::Wait() 可用 双方都无消息泵,无循环依赖风险
纯 Worker 线程等待 UI 线程 ⚠️ 谨慎,需确认 UI 线程当时空闲且不会反向等待 方向性等待,有潜在循环依赖
UI 线程等待任何其他线程 ❌ 禁止直接 Wait UI 线程有消息泵,阻塞会触发 COM/消息死锁
关闭流程中的等待 ❌ 极其危险,SKIP_ON_SHUTDOWN 会让 Signal 永不发出 进程挂死

安全替代方案

替代 1:链式 PostTask(最推荐,全异步)

不需要等待,让操作自然排队:

复制代码
// file 线程清理完后,链式 PostTask 回 UI 线程销毁窗口
file_task_runner_->PostTask(FROM_HERE,
    base::BindOnce([](TaskbarHookManager* manager) {
        // ... 清理逻辑 ...

        // 完成后链式触发 UI 线程销毁窗口
        content::GetUIThreadTaskRunner({})->PostTask(FROM_HERE,
            base::BindOnce(&TaskbarHookManager::DestroyWindowOnUIThread,
                           base::Unretained(manager))
        );
    }, base::Unretained(this))
);
// 不等待,立即返回

替代 2:在 UI 线程上用 base::RunLoop(需要同步语义时)

RunLoop::Run() 启动嵌套消息循环,线程不阻塞消息处理,只阻塞"代码继续向下执行":

复制代码
// 只能在 UI 线程使用
base::RunLoop run_loop;
content::GetUIThreadTaskRunner({})->PostTask(FROM_HERE,
    base::BindOnce([](TaskbarHookManager* self, base::OnceClosure quit) {
        self->DestroyWindowOnUIThread();
        std::move(quit).Run();
    }, base::Unretained(this), run_loop.QuitClosure())
);
run_loop.Run();  // ✅ 消息泵继续工作,COM/窗口消息可正常分发

替代 3:委托给 SequenceChecker + 生命周期管理

如果销毁顺序是已知的(比如 Controller 先于 Manager 析构),在 Controller 的析构序列中保证顺序,而不是在运行时用 WaitableEvent 同步。


核心原则

WaitableEvent::Wait() 等同于在当前线程插入一个"停止一切"的路障。

只要等待的目标(直接或间接)需要当前线程参与(处理消息、分发 COM 调用、执行回调),就必然死锁。

替代思路:把"等完成后做什么"变成一个回调,让完成事件去触发后续操作,而不是主动等待。

这就是 Chromium 架构中所有异步操作的基本范式:PostTaskAndReplyOnceClosureRunLoop

相关推荐
GlueNa2SiO344 分钟前
10-Docker生产环境部署与K8s入门
笔记·学习·docker·容器·kubernetes
茯苓gao1 小时前
无感FOC核心原理:没有编码器,电机如何获得转子电角度?
笔记·嵌入式硬件·学习
天天爱吃肉82182 小时前
【工程师笔记|新能源整车电控一次过CISPR25/BCI,汽车EMC/EMI落地十大核心设计技巧】
大数据·人工智能·笔记·python·汽车
代码地平线2 小时前
C++类与对象(中):默认成员函数与日期类实战
c++·笔记·算法
龙仔7252 小时前
人大金仓Kingbase V8 自动全量备份部署完整笔记
数据库·笔记·备份·人大金仓
怦怦蓝3 小时前
Windows 安装 Docker Desktop 踩坑完整实战指南(WSL2 故障排错实录)
windows·docker·容器
鱼子星_3 小时前
【C++】反向迭代器:反向迭代器的底层认识与模拟实现
开发语言·c++·笔记·stl
xqqxqxxq3 小时前
Redis五大常用数据类型技术笔记
数据库·redis·笔记
chusi7773 小时前
2027考研历年真题+电子备考资料+电子版教材+笔记
笔记·考研
云边有个稻草人4 小时前
复盘小站:用 Doubao-Seed-Evolving,把厚厚的笔记一键变成可复习、可自测、可批改的知识资产
笔记·doubao·ai开发实战·复盘小站