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

相关推荐
RSFeegg2 小时前
【AI Agent 学习笔记 task1】Day2:初识智能体
人工智能·笔记·学习
李长渊哦2 小时前
PostgreSQL 18 本地部署与运维完全指南 (Windows版)
运维·windows·postgresql
小陈phd2 小时前
多模态大模型学习笔记(十八)——基于 DeepSeek-7B 的 LoRA 微调训练实战教程
人工智能·笔记·学习
信鸽爱好者2 小时前
RTX5060显卡+windows CUDA12.8+cuDNN8.9.7+pytorch安装
人工智能·pytorch·windows·深度学习
朗迹 - 张伟2 小时前
UE5.7 基础入门学习笔记
笔记·学习·ue5
不想看见4042 小时前
Find All Numbers Disappeared in an Array数组--力扣101算法题解笔记
笔记·算法·leetcode
handsome09162 小时前
windows电脑用什么代替transporter和xcode
windows·transporter
菜菜小狗的学习笔记2 小时前
黑马程序员java web学习笔记--项目部署(Docker)
java·笔记·学习
程序员杰森2 小时前
ESP32开发板+TB6612 x 2 四电机小车笔记
笔记·单片机·嵌入式硬件