AppFreeze案例:FFRT 与 std::mutex 混用导致的死锁分析
本原创文章帖发布在华为开发者联盟社区,欢迎开发者前往访问评论交流,更多与该内容相关讨论,请点击原帖查看:
1. 背景概述
近期,某三方应用在鸿蒙系统上发生了多例 AppFreeze(应用无响应)事件。系统检测到主线程超时阻塞,强制终止了应用。通过抓取 AppFreeze 日志并结合 FFRT(Function Flow Runtime Kit)的基础理论,我们定位到了导致这一现象的根本原因:在 FFRT 并发模型中错误地使用了 C++ 标准库同步原语(std::mutex 和 std::condition_variable),导致 FFRT Worker 线程陷入内核态阻塞,进而引发任务堆积和死锁。
本文将介绍从日志分析到根因定位的过程,重点解析主线程与 FFRT 线程栈帧中的关键信息。
2. 日志分析与排查思路
2.1 主线程栈分析:UI 线程在等待锁
首先,我们查看 AppFreeze 日志中的主线程快照。主线程(Tid: 21445)
在 THREAD_BLOCK_3S 和 THREAD_BLOCK_6S 两个时间点堆栈一致,均处于阻塞状态。日志中堆栈栈顶与libffrt相关,这时候我们在分析主线程堆栈的时候需要同步考虑查看FFRT 的线程堆栈。
关键堆栈片段:
Tid:21445, Name:XXXX
#00 pc 00000000001d8250 /system/lib/ld-musl-aarch64.so.1(__timedwait_cp+156)
#01 pc 00000000001da328 /system/lib/ld-musl-aarch64.so.1(pthread_cond_timedwait+172)
#02 pc 00000000000c4988 /system/lib64/chipset-sdk-sp/libc++.so(std::__h::condition_variable::wait(std::__h::unique_lock<std::__h::mutex>&)+32)
#03 pc 000000000002aa70 /system/lib64/ndk/libffrt.so(ffrt::mutexPrivate::wait()+456)
#04 pc 000000000002a148 /system/lib64/ndk/libffrt.so(ffrt_mutex_lock+1124)
#05 pc 0000000000218eb0 /data/storage/el1/bundle/libs/arm64/libmtflexbox.so(mt_flexbox::expression::Tool::ParseJson...)
...
分析结论:
1. 阻塞点明确:主线程栈顶为 pthread_cond_timedwait,底层调用链显示其正在等待一个 std::condition_variable(C++ 标准库条件变量)。
2. FFRT 介入:调用链第 3-4 层显示 ffrt::mutexPrivate::wait 和 ffrt_mutex_lock。这表明主线程(虽然它是 UI 线程,但在某些异步回调或混合编程场景下可能通过 FFRT 接口同步等待)正在尝试获取一个 FFRT 管理的互斥锁,而该锁底层映射或关联了标准的 std::mutex/std::condition_variable。
3. 业务上下文:栈帧 #05 显示业务代码位于 libmtflexbox.so 中的 ParseJson 和 ExpressionContext::Init。这暗示主线程在执行动态卡片布局解析时,需要等待某个共享资源(如模板缓存或配置数据)初始化完成,而该初始化过程由 FFRT 任务负责,且使用了错误的同步原语。
2.2 FFRT Worker 线程分析:全部陷入内核等待
接着,我们检查 FFRT 的工作线程(Worker Threads)状态。日志显示多个 FFRT 线程(Tid: 58950, 58951, 58953, 58954 等)均处于完全相同的阻塞状态。
关键堆栈片段:
Tid:58950, Name:OS_FFRT_3_12
state=S, utime=8, stime=1, priority=10, nice=-10, clk=100
#00 pc 00000000001d8250 /system/lib/ld-musl-aarch64.so.1(__timedwait_cp+156)
#01 pc 00000000001da328 /system/lib/ld-musl-aarch64.so.1(pthread_cond_timedwait+172)
#02 pc 00000000000c4424 /data/storage/el1/bundle/libs/arm64/libc++_shared.so(std::__n1::condition_variable::wait(std::__n1::unique_lock<std::__n1::mutex>&)+20)
#03 pc 00000000000c506c /data/storage/el1/bundle/libs/arm64/libc++_shared.so(std::__n1::__assoc_sub_state::wait()+84)
#04 pc 000000000018a27c /data/storage/el1/bundle/libs/arm64/libmtflexbox.so(mt_flexbox::DLDynamicLayout::LoadTemplate(...)+2256)
#05 pc 000000000022aefc /data/storage/el1/bundle/libs/arm64/libmtflexbox.so(...)
#06 pc 000000000006ba30 /system/lib64/ndk/libffrt.so(ffrt::QueueHandler::Dispatch(ffrt::QueueTask*)+388)
#07 pc 00000000000903d4 /system/lib64/ndk/libffrt.so(ffrt::QueueTask::Execute()+372)
#08 pc 000000000004fe04 /system/lib64/ndk/libffrt.so(CoStartEntry(void*)+40)
Tid:58951, Name:OS_FFRT_3_13
(堆栈与 Tid:58950 完全一致)
分析结论:
1. 状态一致:所有 FFRT Worker 线程都停在 pthread_cond_timedwait -> std::condition_variable::wait。
2. 业务栈帧:我们在 FFRT 线程中看到了明确的业务代码 mt_flexbox::DLDynamicLayout::LoadTemplate。这说明 FFRT 线程正在执行“加载模板”的任务,并在任务内部等待某个条件变量被通知。
3. 核心异常:FFRT 线程在 LoadTemplate 中调用了 std::condition_variable::wait。这是一个内核态阻塞操作。当线程等待条件变量时,如果条件不满足,线程会进入内核休眠,不再参与 FFRT 的用户态调度。
4. 死锁闭环:
o FFRT 线程:正在 LoadTemplate 中等待条件变量(例如等待模板加载完成或数据就绪)。
o 主线程:正在等待 FFRT 线程完成 LoadTemplate 并释放锁/通知条件变量(通过 ffrt_mutex_lock 等待)。
o 矛盾点:如果持有锁或负责通知条件变量的逻辑也被阻塞(例如也在等待 FFRT 线程,或者由于锁竞争导致死锁),那么 FFRT 线程将无法醒来,主线程也将等待。
2.3 排查思路总结
1. 现象矛盾:主线程在等 FFRT 任务,而 FFRT 线程也在等(条件变量),但负责“通知”或“释放”的一方似乎没有动作,或者陷入了同样的阻塞。
2. 关键证据:
o 主线程栈顶出现 std::condition_variable::wait。
o FFRT Worker 线程栈顶同样出现 std::condition_variable::wait。
o 业务代码位于 libmtflexbox.so,涉及动态模板加载。
3. 假设验证:开发者在 FFRT 任务(LoadTemplate)中使用了 std::condition_variable 和 std::mutex 进行同步。由于 std::condition_variable 依赖内核同步,导致 FFRT 线程陷入内核休眠,无法被调度去执行“通知”操作,或者因锁竞争形成死锁。
4. 代码定位:检查 libmtflexbox.so 中 DLDynamicLayout::LoadTemplate 的实现,确认其使用了 C++ 标准库同步原语。
3. 根因分析:std::condition_variable 引发的死锁
3.1 问题代码示例
// 错误:在 FFRT 任务中使用了标准库同步原语
std::mutex mtx;
std::condition_variable cv;
bool template_loaded = false;
void task_LoadTemplate() {
std::unique_lock<std::mutex> lock(mtx);
// 错误:在 FFRT 线程中等待条件变量,若无人通知,线程将内核休眠
cv.wait(lock, []{ return template_loaded; });
// ... 后续逻辑 ...
}
void task_NotifyLoad() {
std::lock_guard<std::mutex> lock(mtx);
template_loaded = true;
cv.notify_one(); // 如果此任务也被阻塞或调度失败,通知将不会发出
}
3.2 技术原理剖析
1. 内核态 vs 用户态
• std::condition_variable:底层依赖操作系统的条件变量原语(如 Linux 的 pthread_cond_wait)。当线程调用 wait 时,如果条件不满足,线程会被挂起并陷入内核态休眠。
• FFRT Worker:是轻量级的用户态线程/协程。FFRT 的设计初衷是在用户态高效调度任务,避免频繁的系统调用和上下文切换。
2. 死锁形成机制
在本次故障中,形成了如下闭环:
1. 竞争发生:多个 FFRT Worker 线程(Tid: 58950-58954)同时执行 LoadTemplate 任务。它们尝试获取 std::mutex 并等待 std::condition_variable。
2. 内核态阻塞:由于 std::condition_variable 的特性,等待的 FFRT 线程全部进入内核休眠。此时,FFRT 的用户态调度器(WorkerLooper)失去了这些线程的执行权,它们不再响应 FFRT 的调度指令。
3. 调度停滞:如果负责 notify_one 的逻辑(可能是另一个 FFRT 任务,也可能是主线程)也需要获取同一个 std::mutex,由于该锁已被阻塞的 FFRT 线程持有(或等待获取),notify 操作无法执行。
4. 任务堆积:主线程提交的 UI 刷新依赖任务(或其他高优先级任务)进入 FFRT 队列,但由于大部分 Worker 线程都陷入了内核休眠,FFRT 调度器无法推进任何新任务。
5. 阈值触发:当堆积的任务数量或延迟超过 FFRT 内部定义的硬件阈值(FFTS 阈值)时,系统判定为严重异常。
6. 主线程阻塞:主线程在 ffrt_mutex_lock(底层关联 std::mutex)中等待依赖任务完成,而依赖任务无法被调度执行(因为 Worker 线程在内核休眠),导致主线程进入 pthread_cond_timedwait 死等,最终触发 THREAD_BLOCK_6S 报警,应用被强制退出。
3.3 为什么栈顶是 pthread_cond_timedwait?
这是 std::condition_variable::wait 的标准底层实现。当 FFRT 线程调用标准库的条件变量等待时,系统调用直接暴露在该栈帧中。由于业务栈被内核中断,我们看不到业务代码的后续部分,只能看到 FFRT 调度和标准库同步函数的栈帧。
4. 解决方案
4.1 立即修复:替换同步原语
严禁在 FFRT 任务中使用 std::mutex、std::condition_variable 等 C++ 标准库同步原语。
必须使用 FFRT 提供的原生同步原语,这些原语是为用户态协程调度的,不会导致线程陷入内核休眠。
修正后的代码:
#include <ffrt/ffrt.h>
// 使用 FFRT 原生互斥锁
ffrt_mutex_t mutex_ = ffrt_mutex_init();
// 使用 FFRT 原生条件变量
ffrt_cond_t cond_ = ffrt_cond_init();
bool template_loaded = false;
void task_LoadTemplate() {
ffrt_mutex_lock(&mutex_);
// 正确:使用 FFRT 条件变量,这是协作式的,不会导致线程内核态休眠
while (!template_loaded) {
ffrt_cond_wait(&cond_, &mutex_);
}
ffrt_mutex_unlock(&mutex_);
// ... 后续逻辑 ...
}
void task_NotifyLoad() {
ffrt_mutex_lock(&mutex_);
template_loaded = true;
ffrt_cond_signal(&cond_); // 通知等待的 FFRT 协程/任务
ffrt_mutex_unlock(&mutex_);
}
4.2 其他 FFRT 原生原语
• 互斥锁:使用 ffrt_mutex_t 替代 std::mutex。
• 条件变量:使用 ffrt_cond_t 替代 std::condition_variable。
• 信号量:使用 ffrt_semaphore_t 替代 std::counting_semaphore。
4.3 架构与设计建议
1. 隔离同步域:
o 尽量避免在 FFRT 任务中直接阻塞等待主线程,反之亦然。
o 推荐使用 ffrt::future 和 ffrt::promise 进行异步数据传递和结果回传,这符合 FFRT 的任务依赖模型。
2. 最小化锁粒度:
o 即使使用 ffrt_mutex_t,也应尽量缩小临界区范围。避免在持锁状态下执行 I/O 或耗时计算,以减少其他 FFRT 任务因等待锁而被延迟的风险。
3. 静态代码检查:
o 在 CI/CD 流程中引入静态分析工具,配置规则检测 FFRT 任务上下文中是否使用了 std::mutex、std::condition_variable 等不兼容的同步原语。
4. 监控告警:
o 利用 FFRT 提供的监控接口,实时监控任务队列长度、Worker 线程阻塞时间等指标。当发现大量任务堆积或 Worker 线程长时间处于非业务执行状态时,及时告警。
5. 总结
本次三方应用 AppFreeze 事件的根本原因是并发模型混用导致的死锁。开发者在 FFRT 任务编程模型中错误地使用了面向传统线程模型的 std::mutex 和 std::condition_variable,导致 FFRT Worker 线程陷入内核态阻塞,破坏了 FFRT 的用户态调度机制,进而引发任务饥饿、堆积,最终导致主线程等待超时。
核心教训:在使用鸿蒙系统FFRT 等基于协程/任务的并发框架时,必须严格遵循其提供的同步原语(如 ffrt_mutex_t、ffrt_cond_t),避免混用操作系统级的标准库同步原语,以确保调度效率和系统稳定性。
更多推荐




所有评论(0)