查看: 132|回复: 0

HarmonyOS libuv主线程阻塞与uv_async死循环

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
在 HarmonyOS 应用开发中,libuv 是底层异步 IO 的核心引擎。原文通过两个 Freeze 案例说明:异步框架并不等于业务代码天然安全,主线程调用同步接口、以及 uv_async_t 句柄跨 uv_loop_t 重复初始化,都会引发界面冻结或事件循环死锁。下面按问题现象、堆栈路径、根因和重构方案梳理可落地的排查方法。

一、主线程调用 uv 同步接口导致 UI 卡死

问题现象:应用发送视频文件时界面完全卡死,日志显示主线程阻塞在 uv_fs_sendfile。原文堆栈关键路径为:业务层 ChatViewModel.ts 的 handleSendVideo 触发,经 NAPI 调用 libfs.z.so 的 CopyFile::Sync,最终进入 libuv.so 的 uv_fs_sendfile。该接口是同步文件系统操作,执行期间占用 CPU 并等待 I/O 完成;数据量大或磁盘慢时会长时间切断主线程事件循环,UI 无法刷新。

根因:开发者在主线程(UI 线程)直接调用文件模块同步接口(Sync API)。原文错误写法如下:
  1. // ChatViewModel.ts
  2. async handleSendVideo() {
  3.   // 直接在主线程调用同步拷贝接口,导致主线程阻塞
  4.   const result = await ohos.fileio.copyFileSync(srcPath, destPath);
  5.   console.log("Copy done");
  6. }
复制代码

修复方向:把耗时同步操作移出主线程,使用 taskpool 或 worker。原文给出的 taskpool 重构示例如下:
  1. // 1. 定义任务池任务类
  2. import { taskpool } from '@kit.TaskPoolKit';
  3. class CopyFileTask implements taskpool.Task {
  4.   srcPath: string;
  5.   destPath: string;
  6.   constructor(src: string, dest: string) {
  7.     this.srcPath = src;
  8.     this.destPath = dest;
  9.   }
  10.   execute(): void {
  11.     // 在后台线程执行同步 IO,不影响主线程
  12.     try {
  13.       ohos.fileio.copyFileSync(this.srcPath, this.destPath);
  14.     } catch (e) {
  15.       console.error(`Copy failed: ${JSON.stringify(e)}`);
  16.     }
  17.   }
  18. }
  19. // 2. 业务层调用
  20. async handleSendVideo() {
  21.   try {
  22.     // 提交任务到 taskpool,立即返回,不阻塞主线程
  23.     const task = new CopyFileTask(srcPath, destPath);
  24.     await taskpool.execute(task);
  25.     console.log("Copy task submitted and completed in background");
  26.   } catch (e) {
  27.     console.error(`Task pool error: ${JSON.stringify(e)}`);
  28.   }
  29. }
复制代码

二、PDF 场景下 uv_async 事件循环死锁

问题现象:应用在 taskpool 场景调用第三方 SDK 的 so 接口时必现卡死。堆栈显示线程卡在 libuv 事件循环 uv_run,关键帧包括 uv_async_io、uv_io_poll、uv_run,并出现在 OHOSLoopHandler::OnTriggered 调用链上。

死锁机制:原文通过反编译 libuv.so 源码说明,uv__async_io 会先把 loop->async_handles 移动到临时 queue,再循环取出节点并执行 h->async_cb(h)。如果回调内部再次调用 uv_async_send 或类似操作,使同一个 h 节点重新进入当前正在遍历的 queue,while (!uv__queue_empty(&queue)) 就会持续为真,形成死循环。原文通过 GDB 调试和日志发现,同一个 uv_async_t 句柄在主线程 Loop 和 TaskWorker 线程 Loop 中都被初始化。

根因:SDK 的 LibuvSingleton 初始化存在缺陷。uv_async_init 在每次 getInstance() 后的 initialize 中被调用,没有 call_once 保护;uv_async_t 又与 uv_loop_t 强绑定,跨线程、跨 Loop 重复初始化同一个句柄会破坏内部链表结构,使不同 Loop 的队列相互交织。原文错误代码示意如下:
  1. class LibuvSingleton {
  2. public:
  3.   static LibuvSingleton &getInstance() {
  4.     static LibuvSingleton instance;
  5.     return instance;
  6.   }
  7.   void initialize(uv_loop_t *loop) {
  8.     loop_ = loop;
  9.     // 危险:每次调用都会重新初始化同一个句柄
  10.     // 如果主线程和子线程都调用了 initialize,同一个 async_handle 会被注册到两个不同的 Loop
  11.     uv_async_init(loop, &async_handle, LibuvSingleton::processTask);
  12.     async_handle.data = this;
  13.   }
  14. private:
  15.   uv_async_t async_handle;
  16.   uv_loop_t *loop_;
  17.   // ... 其他成员
  18. };
复制代码

修复方向:确保单例初始化只执行一次,并保证 Loop 与句柄绑定关系正确。原文修复版使用 std::call_once:
  1. #include <mutex>
  2. #include <atomic>
  3. class LibuvSingleton {
  4. public:
  5.   static LibuvSingleton &getInstance() {
  6.     static LibuvSingleton instance;
  7.     return instance;
  8.   }
  9.   // 使用 call_once 确保 initialize 只被调用一次
  10.   void initialize(uv_loop_t *loop) {
  11.     std::call_once(init_flag_, [this, loop]() {
  12.       this->loop_ = loop;
  13.       // 仅在第一次初始化时创建句柄
  14.       // 注意:如果需要在不同 Loop 使用,应创建不同的句柄实例,
  15.       // 而不是复用同一个 static 成员。
  16.       // 此处假设该单例仅服务于单一 Loop 上下文,或需改为线程局部存储
  17.       uv_async_init(loop, &async_handle, LibuvSingleton::processTask);
  18.       async_handle.data = this;
  19.     });
  20.   }
  21.   // ... 其他方法保持不变
  22. private:
  23.   LibuvSingleton() {}
  24.   static std::once_flag init_flag_; // 静态成员,用于控制初始化
  25.   uv_async_t async_handle;
  26.   uv_loop_t *loop_;
  27.   // ...
  28. };
  29. // 静态成员初始化
  30. std::once_flag LibuvSingleton::init_flag_;
复制代码

如果 SDK 确实需要在多个线程或 Loop 中使用异步通知,原文建议不要复用同一个 uv_async_t 实例,可改为 thread_local 存储每个线程自己的 uv_async_t 句柄;或在 initialize 中检查句柄是否已初始化,已初始化则复用,但必须确保 loop 相同,否则报错。

三、排查清单与工程约束

原文给出的排查线索包括:应用卡死在 uv_run 或 uv_io_poll;创建了多个 napi_threadsafe_function 或 uv_async_t,但回调偶尔不执行或执行异常;堆栈中出现链表遍历相关的死循环。遇到这些现象,应检查是否存在多 Loop 重复初始化问题。

落到工程实践,可归纳为四点:主线程零阻塞,任何文件 IO、网络请求、复杂计算的同步操作必须使用 taskpool、worker 或异步 API;单例初始化保护,涉及系统底层句柄(如 uv_async_t、pthread_mutex 等)的单例,务必使用 std::call_once 或互斥锁保护,防止多线程竞态;句柄生命周期管理,uv_async_t 等句柄与 uv_loop_t 强关联,切勿在多个不同的 uv_loop_t 中初始化同一个句柄实例;代码审查重点,在 Code Review 时重点关注 Native C++ 层中全局变量、静态变量在多线程环境下的初始化逻辑。

从这两个案例可以推导:HarmonyOS 应用即便使用异步框架,也要区分“框架异步”和“当前调用线程是否阻塞”;Native 层单例与系统句柄的初始化,必须按 Loop/线程边界进行设计。否则问题往往不在 ArkTS 业务代码表面,而会在 libuv 事件循环和 NAPI 调用链中表现为 Freeze。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-10-22 09:56 , Processed in 0.031319 second(s), 29 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部