Swift5中如何无阻塞并行读取Process的standardOutput与standardError?
在 Swift 5 中使用 Process 执行外部命令时,若通过 for try await line in Pipe.fileHandleForReading.bytes.lines 分别读取标准输出(stdout)和标准错误(stderr),可能出现其中一个读取任务阻塞另一个的情况。比如示例代码中,stderr 的读取任务启动后,stdout 的输出会停止,只有当 stderr 有数据写入时(如断开WiFi触发错误输出),stdout 才会恢复。
问题根源
示例代码中,processStdOut 和 processStdErr 先执行了延迟睡眠,导致管道缓冲区被子进程的输出填满,子进程因此被阻塞,无法继续向另一个管道写入数据。此外,bytes.lines 的顺序读取特性,若其中一个管道无数据,读取任务会一直等待,间接影响子进程的运行状态。
可行解决方法
1. 立即启动读取任务,避免延迟
移除读取函数中的睡眠逻辑,在 process.run() 后立刻并发启动两个读取任务,确保管道缓冲区能及时被清空,子进程不会被阻塞。
修改后的读取函数:
func processStdOut() async { do { for try await line in outputPipe.fileHandleForReading.bytes.lines { print("stdout Line: \(line)") } } catch { NSLog("processStdOut Error \(error.localizedDescription)") } NSLog("processStdOut finished") } func processStdErr() async { do { for try await line in errorPipe.fileHandleForReading.bytes.lines { print("stderr Line: \(line)") } } catch { NSLog("processStdErr Error \(error.localizedDescription)") } NSLog("processStdErr finished") }
2. 结合 GCD 的 readabilityHandler 实现非阻塞读取
使用 FileHandle 的 readabilityHandler 回调,基于 GCD 监听管道数据,避免 bytes.lines 可能的阻塞问题,同时可以和 async/await 结合使用。
示例实现:
func processStdOut() async { return await withCheckedContinuation { continuation in let fileHandle = outputPipe.fileHandleForReading fileHandle.readabilityHandler = { handle in do { guard let data = try handle.read(upToCount: 1024), !data.isEmpty else { handle.readabilityHandler = nil continuation.resume() return } if let content = String(data: data, encoding: .utf8) { print("stdout Content: \(content)") } } catch { NSLog("processStdOut Error \(error.localizedDescription)") handle.readabilityHandler = nil continuation.resume() } } } } func processStdErr() async { return await withCheckedContinuation { continuation in let fileHandle = errorPipe.fileHandleForReading fileHandle.readabilityHandler = { handle in do { guard let data = try handle.read(upToCount: 1024), !data.isEmpty else { handle.readabilityHandler = nil continuation.resume() return } if let content = String(data: data, encoding: .utf8) { print("stderr Content: \(content)") } } catch { NSLog("processStdErr Error \(error.localizedDescription)") handle.readabilityHandler = nil continuation.resume() } } } }
3. 进程结束后主动关闭文件句柄
在任务组中,等待进程退出后主动关闭两个管道的读取句柄,让 bytes.lines 的循环能够正常结束,避免读取任务无限等待。
修改后的任务组代码:
await withTaskGroup(of: Void.self) { group in group.addTask { await processStdErr() } group.addTask { await processStdOut() } group.addTask { process.waitUntilExit() // 主动关闭句柄,终止读取循环 try? outputPipe.fileHandleForReading.close() try? errorPipe.fileHandleForReading.close() } }
核心思路
解决此类问题的关键在于确保两个管道的读取操作并发且及时执行,避免子进程因管道缓冲区溢出被阻塞。无论是直接使用 async/await 的 bytes.lines,还是结合 GCD 的回调,核心都是保证数据能被及时读取,不让子进程陷入等待状态。
内容的提问来源于stack exchange,提问作者karl

