使用io.Pipe接收FFmpeg执行结果时程序冻结问题排查
解决exec.Cmd配合io.Pipe时的冻结与无数据问题
问题根源分析
1. 程序冻结的原因
io.Pipe的读写操作是完全阻塞式的:当FFmpeg进程往io.PipeWriter写入数据时,如果没有goroutine从对应的io.PipeReader读取,写操作会一直阻塞。而你在主goroutine中调用cmd.Wait()时,主goroutine会等待FFmpeg进程退出,但FFmpeg因写Pipe被阻塞无法继续执行,最终形成双向死锁:主goroutine等FFmpeg结束,FFmpeg等Pipe有空间写数据,双方都卡住。
2. 移除Wait后无数据的原因
cmd.Wait()是用来等待子进程完全执行完毕的,移除它后,主goroutine会直接继续执行(甚至退出),此时FFmpeg进程可能还没来得及往Pipe写入数据就被终止,导致Pipe中没有内容。
解决方案
核心是协调读Pipe的goroutine、子进程启动、Wait操作和Pipe关闭的顺序,具体步骤:
- 先启动读Pipe的goroutine,确保子进程写入时能被及时读取,避免阻塞
- 子进程启动后,不能立即在主goroutine调用Wait,要等读操作的goroutine做好准备
- 子进程执行完毕后,必须关闭
io.PipeWriter,让读端收到EOF信号,避免读goroutine一直阻塞
修正后的代码示例
func TestCmdRunner(t *testing.T) { pr, pw := io.Pipe() defer pr.Close() var wg sync.WaitGroup wg.Add(1) // 先启动读数据的goroutine go func() { defer wg.Done() buf := make([]byte, 1024) for { n, err := pr.Read(buf) if err == io.EOF { break } if err != nil { t.Errorf("读取Pipe出错: %v", err) return } // 这里处理预览数据,比如写入临时文件或解析视频帧 _ = buf[:n] } }() runner := NewCmdRunner() // 注意给FFmpeg指定输出格式(比如-f mp4),否则它可能不会输出到stdout cmd := runner.BuildCmd("ffmpeg", []string{"-i", "input.mp4", "-f", "mp4", "-"}, pw) if err := cmd.Start(); err != nil { t.Fatal(err) } // 单独goroutine等待子进程完成,完成后关闭PipeWriter go func() { if err := cmd.Wait(); err != nil { t.Errorf("FFmpeg执行出错: %v", err) } pw.Close() // 关闭写端,让读端收到EOF }() // 等待读操作完成 wg.Wait() }
额外注意点
- FFmpeg默认可能不会将输出写入stdout,必须通过
-f <格式>参数指定输出格式,同时用-表示输出到stdout - 必须确保
io.PipeWriter被关闭,否则读goroutine会一直阻塞在Read操作上 - 不要在主goroutine中同时做启动子进程和等待子进程的操作,要利用goroutine拆分阻塞操作
内容的提问来源于stack exchange,提问作者alex
相关产品推荐
相关产品推荐

