场景还原
代码(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。
安全条件:
- 当前线程没有消息泵(不是 UI 线程、STA COM 线程、任何需要处理消息的线程)
- 等待的任务不依赖当前线程或当前线程上的任何资源(无循环依赖)
- 等待的任务保证一定会 Signal(无死路)
这里的潜在问题
情形 1:当前线程是 file 线程,UI 线程正常,看起来安全
file 线程: window_destroyed.Wait() ← 阻塞
↓
UI 线程: DestroyWindowOnUIThread() → done->Signal()
↓
file 线程: 继续执行
表面上无死锁,但存在以下隐患:
隐患 A:UI 线程可能已经在等待 file 线程
如果 Uninitialize 本身是被 UI 线程发起的(CleanupHooks 有 DCHECK_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 架构中所有异步操作的基本范式:
PostTaskAndReply、OnceClosure、RunLoop。