背景
在 Chromium 浏览器中,实现任务栏美化功能需要向 explorer.exe 注入 DLL,通过 XAML TAP(Thread-Aware Provider)接口控制任务栏外观。注入函数 pfnInjectTAP_ 在内部调用 Windows 的 InitializeXamlDiagnosticsEx API 建立跨进程 XAML 诊断连接。
死锁复现
问题代码
初始化路径(CreateWindowOnUIThread,在 UI 线程执行):
// 在 UI 线程上调用
void TaskbarHookManager::CreateWindowOnUIThread() {
// 创建 workerWindow_...
base::WaitableEvent injection_done(...);
// 把注入任务派发到 file 线程
file_task_runner_->PostTask(FROM_HERE,
base::BindOnce([](TaskbarHookManager* self, base::WaitableEvent* done) {
self->PerformInjection(); // 内部调用 pfnInjectTAP_
done->Signal();
}, this, &injection_done)
);
injection_done.Wait(); // ← UI 线程在此阻塞,消息泵停止
}
注入函数(InjectExplorerTAP,在 file 线程执行):
// 在 file 线程上执行
HRESULT InjectExplorerTAP(HWND window, REFIID riid, LPVOID* ppv) {
DWORD tid = GetWindowThreadProcessId(window, &pid);
// 1. 用 SetWindowsHookEx 将 DLL 注入 explorer 的任务栏线程
wil::unique_hhook hook(SetWindowsHookEx(WH_CALLWNDPROC, CallWndProc,
hModule, tid));
auto event = TAPSite::GetReadyEvent();
// 2. 等待 explorer 内 TAP 就绪信号(最多 35 秒)
if (!event.wait(35000)) { // ← file 线程在此阻塞
return HRESULT_FROM_WIN32(WAIT_TIMEOUT);
}
...
}
explorer 侧 TAP 初始化(TAPSite::Install,在 explorer 线程执行):
DWORD TAPSite::Install(void*) {
// 调用 InitializeXamlDiagnosticsEx 建立 XAML 诊断连接
// 内部通过 COM STA 向调用发起方(Chrome 进程)的 UI 线程发消息进行握手
hr = ixde(conn.c_str(), pid, nullptr, location.c_str(), ...);
// 握手完成后才 Signal 就绪事件
event.SetEvent();
}
死锁闭环
Chrome UI 线程
[状态:WaitableEvent::Wait() 阻塞,消息泵停止]
│
│ 等待 file 线程完成注入
▼
Chrome file 线程
[状态:event.wait(35000) 阻塞,等待 explorer TAP 就绪信号]
│
│ 等待 explorer 发出 TAP ready event
▼
explorer 任务栏线程
[状态:InitializeXamlDiagnosticsEx 内部通过 COM STA 等待 Chrome UI 线程响应]
│
│ 需要 Chrome UI 线程处理 COM 消息(STA 代理/存根握手)
▼
Chrome UI 线程
[消息泵已停止,COM 消息无法分发]
│
└── 死锁!三方互等,35 秒后 WAIT_TIMEOUT,注入失败
根本原因
| 层次 | 错误 |
|---|---|
| 架构层 | 在 UI 线程上调用阻塞性等待 WaitableEvent::Wait(),违反 Chromium UI 线程不可阻塞原则 |
| 系统层 | InitializeXamlDiagnosticsEx 基于 COM STA(单线程公寓)机制,跨进程调用需要双方都有活跃的消息泵;UI 线程被阻塞后消息泵停止,COM 握手无法完成 |
| 设计层 | 把依赖 UI 消息泵的跨进程操作(DLL 注入 / COM 初始化)放到需要同步等待的路径上 |
修复原则
UI 线程永远不能调用阻塞性等待,必须使用以下替代方案:
方案 A:纯异步(最简洁)
void TaskbarHookManager::CreateWindowOnUIThread() {
// 创建 workerWindow_...
// ✅ 直接 PostTask,不等待,UI 线程立即返回
if (file_task_runner_) {
file_task_runner_->PostTask(FROM_HERE,
base::BindOnce(&TaskbarHookManager::PerformInjection,
base::Unretained(this))
);
}
// 不调用 Wait()
}
后续操作(颜色设置等)同样 PostTask 到 file_task_runner_,天然排在注入任务之后,无需等待通知。
方案 B:需要感知完成 ------ 链式 PostTask 回调
void TaskbarHookManager::PerformInjection() {
// ... 注入逻辑 ...
// 注入完成后,链式回调到 UI 线程
content::GetUIThreadTaskRunner({})->PostTask(FROM_HERE,
base::BindOnce(&TaskbarHookManager::OnInjectionReady,
base::Unretained(this))
);
}
方案 C:必须同步等待 ------ 用 base::RunLoop 替代 WaitableEvent::Wait()
// ✅ RunLoop 启动嵌套消息循环,UI 线程仍能处理消息
base::RunLoop run_loop;
file_task_runner_->PostTask(FROM_HERE,
base::BindOnce([](TaskbarHookManager* m, base::OnceClosure quit) {
// ... 清理 ...
std::move(quit).Run();
}, base::Unretained(this), run_loop.QuitClosure())
);
run_loop.Run(); // 不阻塞消息泵,COM 通信正常
推论:Uninitialize 有完全相同的问题
Uninitialize() 中 taskbarService_->RestoreAllTaskbarsToDefault() 同为 COM STA 调用,在 UI 线程通过 cleanup_done.Wait() 等待,同样会死锁。修复时还需注意资源销毁顺序 :workerWindow_ 的销毁必须在 file 线程清理(Release + UnloadDLLs)完成后再执行,否则存在并行竞态,应使用链式 PostTask 保证顺序:
UI 线程: PostTask(file, 清理) → 立即返回
↓
file 线程: COM 还原 → Unhook → UnloadDLLs → PostTask(UI, DestroyWindow)
↓
UI 线程: DestroyWindowOnUIThread() ← 在所有清理完成后才执行
教训
在 UI 线程(或任何有消息泵的线程)上等待依赖消息泵的跨线程/跨进程操作,必然死锁。
COM STA、
SetWindowsHookEx、XAML 诊断连接等机制都隐式依赖消息泵,需格外警惕。