嵌入式Linux——开发踩坑记:从 WebSocket 死锁到 PortAudio 音频丢包的硬核调试
本文记录了开发Linux AIChat客户端时遇到的三个典型Bug及其解决方案:1)通过原子变量实现异步通信解决WebSocket死锁问题;2)采用混合轮询模式平衡阻塞与非阻塞读取,避免音频采集线程死锁;3)正确处理ALSA驱动溢出错误,确保录音数据不丢失。这些案例揭示了多线程同步、驱动交互和错误处理的深层原理,为嵌入式音频应用开发提供了宝贵经验。
为Linux开发板开发AIChat 客户端“踩坑”实录:从死锁到静音的排查之旅
这篇文档记录了我在开发 AIChat 客户端时遇到的三个Bug。为了方便理解,我让ai把枯燥的代码逻辑比作生活中的场景,并标注了背后的硬核技术点和具体的代码实现。
第一关:TTS End 死锁 —— “电话亭与送货员”的故事
🛠️ 涉及技术点:
- 并发编程:多线程 (
std::thread)、互斥锁 (std::mutex)、原子变量 (std::atomic)- 网络编程:WebSocket 协议 (
websocketpp)、异步回调 (Async Callback)
1. 场景设定
- WebSocket 连接锁 = 电话亭的钥匙
- 采集线程 = 送货员
- WebSocket 回调线程 = 老板
2. 案发现场(死锁是怎么发生的?)
老板(WS 回调线程)拿着钥匙进电话亭接电话,想等送货员(采集线程)下班。送货员拿着货想进电话亭发货,不给钥匙就不下班。两人互相等待,导致死锁。
3. 破局之道(异步停止)
我们规定:老板接电话时,绝对不能在电话亭里等送货员!
💡 形象比喻
老板接到“TTS 结束”电话,只在门口贴张条子:“送货员,一会儿下班啊”,然后立刻交出钥匙走人。主循环看到条子后,在外面等着送货员下班。
💻 代码实现
“条子”的定义 (Application.h):
// 这是一个原子变量,保证多线程读写的安全性(不需要额外的锁)
std::atomic<bool> pending_force_idle_{false};
“贴条子” (Application.cc):
void Application::HandleTtsStreamEnd() {
// 不直接调用 Stop(),因为那会死锁
// 而是设置原子标志位(贴条子)
pending_force_idle_ = true;
}
“看条子” (Application.cc):
void Application::MainLoop() {
while (running_) {
// 主循环每轮检查一次条子
if (pending_force_idle_) {
pending_force_idle_ = false; // 撕掉条子
StopListeningAudio(); // 安全地停止录音(此时没拿 WS 锁)
state_machine_->ChangeState(GetIdleState());
}
// ...
}
}
第二关:Listening 超时死锁 —— “叫不醒的装睡人”
🛠️ 涉及技术点:
- 音频开发:PortAudio 库、ALSA 驱动交互
- I/O 模型:阻塞 I/O (
Blocking I/O) vs 非阻塞轮询 (Non-blocking Polling)- 线程同步:
std::thread::join()
1. 场景设定
- Pa_ReadStream (阻塞读) = 进仓库等货
- Pa_AbortStream (终止流) = 老板的大喇叭
2. 案发现场
送货员进仓库(阻塞读)等货,但因为驱动 Bug,老板在外面喊破喉咙(Abort)他也听不见。老板在门口死等(join),导致死锁。
3. 破局之道(从“轮询”到“混合模式”的演进)
我们经历了两个阶段才彻底解决这个问题。
第一阶段:纯轮询 (Pure Polling) —— “礼貌的送货员”
我们规定:送货员绝对不能进仓库死等,必须先问问有没有货。
- 做法:送货员每 5 毫秒问一声看门大爷(
Pa_GetStreamReadAvailable):“有货吗?”- 有货 -> 进仓库搬。
- 没货 -> 玩手机。
- 结果:死锁解除了!(因为送货员一直在门口玩手机,老板一叫就能应)。
- 新问题:饿死(Starvation)。因为看门大爷(驱动)撒谎,一直说“没货”,送货员就一直玩手机,结果一点声音都没录到。
第二阶段:混合模式 (Hybrid Mode) —— “急眼的送货员”
我们规定:送货员不能太老实,如果等太久了,不管大爷说什么,都要冲进去看看。
- 做法:引入一个计数器。如果连续 20 次(约 100ms)大爷都说没货,送货员就强制冲进仓库搬一次。
- 结果:既避免了死锁(大部分时间在玩手机),又保证了能录到声音(定期强制搬货)。
💻 代码实现
关键变量定义 (AudioProcess.h):
PaStream *stream_; // PortAudio 流句柄(仓库)
std::atomic<bool> capturing_; // 录音控制标志(老板的命令)
“混合模式”逻辑 (AudioProcess.cc):
void AudioProcess::CaptureLoop() {
int poll_count = 0;
while (capturing_) {
// 1. 问大爷:有货吗?
long available = Pa_GetStreamReadAvailable(stream_);
// 2. 没货(或者货不够)
if (available < frame_samples_) {
poll_count++;
// 如果还没等到急眼(< 20次)
if (poll_count < 20) {
std::this_thread::sleep_for(std::chrono::milliseconds(5)); // 玩手机
continue;
}
// 否则:急眼了!强制往下走(去搬货)!
} else {
poll_count = 0; // 有货,计数器清零
}
// 3. 进仓库搬货(可能是因为有货,也可能是因为急眼了)
Pa_ReadStream(stream_, ...);
}
}
第三关:录音静音 —— “撒谎的侦察兵”与“爆仓”
🛠️ 涉及技术点:
- 缓冲区管理:缓冲区溢出 (
Buffer Overflow)、饿死 (Starvation)- API 调试:错误码分析 (
Error Handling)
1. 场景设定
- Pa_GetStreamReadAvailable = 看门大爷(侦察兵)
- paInputOverflowed = 爆仓警告单
2. 案发现场
大爷(驱动)老眼昏花,一直说“没货”,导致送货员一直玩手机。等我们强制送货员进去搬货时,发现货堆满了(Overflow),送货员吓得把货全扔了(continue),导致服务器收不到声音。
3. 破局之道(容忍溢出)
我们告诉送货员:“爆仓警告单”不是“报废单”!
💡 形象比喻
只要搬出来的货是好的,不管有没有收到“爆仓警告单”,都要发给服务器。
💻 代码实现
“无视警告单” (AudioProcess.cc):
// 强制去搬货后...
PaError err = Pa_ReadStream(stream_, ...);
if (err != paNoError) {
// 以前:看到错误直接扔货 (continue)
// 现在:如果是爆仓警告,仅仅打印个日志,货照样发!
if (err == paInputOverflowed) {
std::cerr << "爆仓了,但货还要发!" << std::endl;
// 不写 continue,让代码继续往下走
} else {
// 其他严重错误才扔货
continue;
}
}
// 继续编码、发送...
EncodeAndSend(buffer);
总结
- 电话亭死锁:用
std::atomic<bool>实现了无锁的异步通信。 - 仓库死锁:用
Pa_GetStreamReadAvailable+ 混合模式 实现了既防死锁又防饿死的平衡。 - 静音乌龙:用
paInputOverflowed的特殊处理挽救了数据。
这一路走来,我们不仅修好了 Bug,更深刻理解了多线程(电话亭)、驱动交互(仓库)和错误处理(警告单)的奥义。
更多推荐

所有评论(0)