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

相关推荐
华山沦贱10 小时前
open62541 V1.5.4版对C++ Builder支持的bug
笔记
x***r15110 小时前
Redis Desktop Manager 0.8.8 安装教程(Windows redis-desktop-manager-0.8.8.384详细步骤)
数据库·windows·redis
稷下元歌10 小时前
七天学会plc 加机器视觉完整笔记:S7-1200 数据类型、存储区与寻址方式(I/Q/M/DB 详解)。
网络·数据库·笔记
逸模11 小时前
AI+BIM 重构连锁公装新范式 逸模打造数字化营建核心底座
大数据·人工智能·笔记·其他·信息可视化·重构
xqqxqxxq11 小时前
树结构技术学习笔记
数据结构·笔记·学习
十月的皮皮12 小时前
C语言学习笔记202606008- 三角形判断(3种方法)
c语言·笔记·学习
XGeFei12 小时前
【Fastapi学习笔记(6)】—— Fastapi文件上传、请求头自动转换
笔记·学习·fastapi
嘶哈哈哈12 小时前
嘉立创 EDA 入门实操笔记:从原理图到 PCB 布线、差分对、覆铜与 DRC 检查
开发语言·笔记·php
一口吃俩胖子12 小时前
【脉宽调制DCDC功率变换学习笔记024】频域性能
笔记·学习
吃着火锅x唱着歌12 小时前
深度探索C++对象模型 学习笔记 第五章 构造、解构、拷贝语意学(2)
c++·笔记·学习