死锁案例:UI 线程阻塞等待跨进程 COM 注入

背景

在 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 诊断连接等机制都隐式依赖消息泵,需格外警惕。

相关推荐
zhangrelay8 小时前
新版|-|单片机原理与接口技术-2026持续改进优化-从MEGA2560-ESP32与Wokwi到ROS2-Lyrical与具身智能栈
笔记·单片机·嵌入式硬件·学习
Jimmy8 小时前
深夜突袭:我们的午夜战场
笔记·产品·电子书
xiaoye-duck9 小时前
《C++ 工具库实战》gflags 命令行参数解析库详解
c++·第三方库
狂奔的sherry10 小时前
C++ 宏生成访问函数的代价:访问权限、调试和类型检查
c++·宏·编译·预处理器·类型检查
阿尼卡~10 小时前
VS Code 配置 C 语言开发环境(Windows 10/11):从 GCC 安装到运行、断点调试,一篇搞定
c语言·开发语言·windows
志尊宝10 小时前
AI原生前端开发(025):端侧AI实战项目:智能笔记助手
人工智能·笔记·ai-native
lll235269177911 小时前
线下多人发言对比与录音操作
笔记·智能手机
划破黑暗的第一缕曙光11 小时前
[C++]:5.STL:string(2)
java·c++·redis
叼烟扛炮11 小时前
Linux 学习笔记(三十三):线程互斥——互斥锁(Mutex)与死锁
linux·运维·服务器·笔记·学习·1024程序员节
小超人诞生日记14 小时前
C++ 异常机制详解:从抛出捕获到异常安全
c++·算法