QProcess 把「拼引号」和「接管道」都包好了,只留下一件事要你自己做: 两路输出都得读,读的时候别把界面冻住。
上一节的 CreateProcess 是裸接口,这一节的 QProcess 是包装过的。 包装省掉了拼串,但没省掉管道那 4 KB——坑还在,只是换了个样子。
一、QProcess 替你做了两件事
QProcess::start(program, arguments) 这个重载接受 QStringList。
它内部按平台规则拼命令行,所以你永远不用碰引号。
这是它比 system() 与裸 CreateProcess 都省事的地方。
第二件事是管管道:它把 stdout 与 stderr 各接一根, 再用信号告诉你「有新数据了」。 于是「边读边等」这件事,从手写循环变成了连一个信号。
但要注意:它只是给了你两根管子,读不读还是你的事。 这一点在后面会变成最常见的一种卡死。
二、只连 stdout,等于没连
ffmpeg 的进度与日志全在 stderr 上——这是第二章就定下的规矩。
而 Qt 默认把两路分开,只连 readyReadStandardOutput 的人,
stdout 收得干干净净,stderr 一根字节都没人读。
没人读的管道就是一只不排水的桶。
ffmpeg 往里写,写满 4 KB 就写不动了,于是它停住;
你的程序在 waitForFinished() 上等它退出,于是你也停住。
界面上什么都不显示,任务管理器里两个进程都活着,CPU 几乎为零——
这就是死锁的样子。
本机实测那条管子的容量:写 4096 字节能出来,写 4608 字节就卡住。
换个说法:ffmpeg 打十几行 frame= ... 进度,就够把管子填满了。
三、三条正路,按场合选
① 两路都连信号:两个 readyRead 信号都要连上。
各自 readAll() 追加到日志,这是唯一能实时显示进度的写法。
② 合并成一路:加 setProcessChannelMode(MergedChannels)。
stderr 会并进 stdout,只需连一个信号。
代价是两路分不开了——但你要的本来就是一整段日志。
③ 阻塞式读:while (waitForReadyRead(100)) 循环。
边读边等,直到进程结束。
适合后台任务,不适合带界面的场景——因为它会占住事件循环。
三条路的共同点是「读」和「等」交织在一起。 错的那条路只有一个特征:先等后读。
四、进度怎么解析
ffmpeg 的进度行是用回车覆盖的:frame= 120 fps= 30 q=28.0 size= ...,
每更新一次就在同一行重写。
所以拿到的不是一行一行的文本,而是一串带 \r 的碎片。
按 \n 切行会切不出东西,得按 \r 切。
更稳的做法是让 ffmpeg 输出结构化进度:加 -progress pipe:1 -nostats,
它会按固定格式打 out_time_ms=...、frame=...,一行一个键值对。
这比解析人类可读的进度行可靠一个数量级。
五、取消:先礼后兵
Qt 里停一个子进程有两个方法:terminate() 是「请你自己退出」,kill() 是「立刻干掉」。
ffmpeg 收到优雅退出请求时会把文件尾写完整,直接杀掉则可能留下半截 mp4。
所以顺序应该是:terminate() → 等两秒 → 还在就 kill()。
对应到命令行,也可以让 ffmpeg 自己去监听 stdin 的 q——
但那需要你给它一根可写的 stdin,而且要注意 -nostdin 会关掉这条路。
六、一句总结
QProcess 把「怎么起进程」这件事做完了, 「怎么读、什么时候等、怎么停」还是你的活。 而这三件事在任何 GUI 框架里都一样: 读到的东西要及时取走,等待要放在不冻界面的地方,停止要留出善后的时间。
七、动手 Demo:只等不读,到底会卡在哪一秒
让子程序往 stdout 写 300 KB,父进程只等不读。 再用同样的数据量改成「边读边等」,两行结果对照着看。
import subprocess, sys, time
PY = sys.executable
BIG = "import sys; sys.stdout.write('x' * 300000); sys.stdout.flush()"
p = subprocess.Popen([PY, "-c", BIG],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
try:
p.wait(timeout=2)
print("只 wait:居然出来了")
except subprocess.TimeoutExpired:
print("只 wait:卡住")
p.kill(); p.wait()
for n in (4096, 4608):
q = subprocess.Popen(
[PY, "-c", "import sys; sys.stdout.write('x'*%d)" % n],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
try:
q.wait(timeout=1.5)
print("%d 字节:出来了" % n)
except subprocess.TimeoutExpired:
print("%d 字节:卡住" % n)
q.kill(); q.wait()
实跑输出:
--------------------------------------------------------------------
12.1.3 Qt 调用 FFmpeg 命令行
--------------------------------------------------------------------
**① 只等不读,会永久卡住——这是 Qt 里最常见的死法**
**让子程序往 stdout 写 300 KB,父进程只 `wait()`、一个字节都不读:**
父进程怎么做的 结果 收到多少字节
------------------------------------------------------------------
只 wait,不读 卡住,3 秒还没结束 0
边读边等(communicate) 正常结束,656 毫秒 300000
**这不是死循环,是【互相等】:子进程等管道腾地方,父进程等子进程退出。**
**两边都不肯先动,就永远停在那儿。**
**在 Qt 里表现成 `waitForFinished()` 一直不返回,界面整个冻住,**
**用户以为程序挂了,其实它在老老实实等。**
**② 管道到底能存多少?测一下就知道**
**同样的写法,只改子进程写的字节数,二分一下临界点:**
子进程写多少 只 wait 会怎样
------------------------------------------------
1024 字节 正常结束
4096 字节 正常结束
4608 字节 卡住
8192 字节 卡住
65536 字节 卡住
**4096 出得去,4608 就卡住——管道缓冲的大小在【4 KB 到 4.5 KB】之间。**
**这个数看着小得离谱,但它是操作系统给匿名管道配的默认大小,**
**不是谁写错了。**
**换句话说:ffmpeg 只要往 stderr 打十几行进度、往 stdout 吐一点数据,**
**攒够 4 KB,你不读,它就得停。**
**③ Qt 里的三条正路**
做法 怎么写 适合什么场合
--------------------------------------------------------------------------------------
两路都连信号 connect(readyReadStandardOutput…) 要实时显示进度
合并成一路 setProcessChannelMode(MergedChannels) 只要日志、不分流
阻塞式读 waitForReadyRead 循环 后台任务,不要界面
**第一条的要点是【两个信号都要连】:只连 stdout、不管 stderr,**
**ffmpeg 把进度全打在 stderr 上,照样 4 KB 就塞满。**
**第二章那句「ffmpeg 的进度走 stderr」在这里变成了真金白银的 bug。**
**④ 对应的 C++ 骨架(本机没有 Qt,这段是写给你抄的)**
QProcess p;
p.setProcessChannelMode(QProcess::MergedChannels); // 两路合一,最省事
p.start("ffmpeg", args); // args 是 QStringList,空格天然安全
if (!p.waitForStarted(5000)) { /* 起不来 */ }
while (p.waitForReadyRead(100)) { // 边读边等,绝不裸 wait
log += p.readAll();
}
p.waitForFinished(-1);
int rc = p.exitCode(); // 0 与非 0
**注意 `p.start("ffmpeg", args)` 这个重载:它接受 `QStringList`,**
**内部自己拼命令行——所以你不需要管引号,这正是「用库」比「用 system」省事的地方。**
**而 `while (waitForReadyRead)` 那三行,就是防死锁的全部秘密。**
第一行卡住,第二、三行把临界点夹出来了:4096 出得去,4608 就卡。
所以在 Qt 里,ffmpeg 只要打十几行进度,waitForFinished() 就能一直不返回。
这不是 Qt 的 bug,是管道的容量;换成 Java、Python,数字一个字节都不变。
30 秒实验
- 把
stderr=subprocess.PIPE改成stderr=subprocess.STDOUT:看还卡不卡。 - 把子程序改成先写 4 KB 再睡 3 秒:看「边读边等」能不能提前处理。
- 给 ffmpeg 加
-progress pipe:1 -nostats:看进度格式变了什么。
常见误区
- 只连
readyReadStandardOutput。ffmpeg 的日志全在 stderr,那根管子永远没人读。 - 在 UI 线程里
waitForFinished()。界面直接冻住,用户以为程序死了。 - 直接
kill()取消。mp4 的尾没写完,文件成了坏的。
自测三问(点开对答案)
1. QProcess 省掉了哪一步? —— 拼命令行(引号)。它接受 QStringList,按平台规则自己拼。 2. 管道满了以后,子进程为什么会停? —— 写操作阻塞到有空间为止。没人读就永远没空间。 3. 为什么优先用 `-progress pipe:1`? —— 它输出结构化的键值对,不用解析带 `\r` 的人类可读进度行。- ☐ 12.1.3 完——下一节 12.1.4 MinGW 调用 FFmpeg 命令行