Win32 ReadFile函数在无关线程上阻塞的原因排查
因命令行长度超过8191字符,无法使用popen/pclose,转而采用Win32的CreateProcess API实现多线程启动外部进程并捕获输出,流程如下:
- 创建N个C++
<thread>线程; - 线程获取任务后启动带指定参数的外部进程;
- 调用
WaitForSingleObject等待进程结束; - 调用
ReadFile读取输出管道直至为空; - 关闭所有句柄,返回步骤2检查任务。
异常现象:所有线程启动进程后,所有线程的ReadFile会阻塞,直到每个线程都执行到该步骤后阻塞才解除。
最小示例(MWE)包含模拟外部进程的sleepHelper.exe,主程序可指定线程数——一个线程对应5秒任务,其余对应500毫秒任务,运行输出如下:
.\MinimalExample.exe 3 Using thread count = 3 Thead 3: Waiting for object took 525ms. Thead 2: Waiting for object took 525ms. Thead 3: Reading output took 4489ms. Thead 2: Reading output took 4489ms. Thead 3: Executing process took 5022ms. Thread 3 is exiting. Thead 2: Executing process took 5022ms. Thread 2 is exiting. Thead 1: Waiting for object took 5015ms. Thead 1: Reading output took 0ms. Thead 1: Executing process took 5023ms. Thread 1 is exiting. Total runtime was 5025ms.
核心问题:
- 为何此处
ReadFile会阻塞? - 为何所有线程到达
ReadFile步骤时阻塞会解除?
注:已知可先调用PeekPipe再读取,但微软文档仅使用ReadFile且该场景下线程全部到达后可运行,故迫切想了解原因;技术上,线程先成功读取49字节后挂起,最终返回读取0字节。
1. ReadFile阻塞的原因
你在调用WaitForSingleObject等待子进程结束后才读取管道,但子进程退出时,其输出管道的写端并未完全关闭:使用CreateProcess创建带管道的子进程时,子进程会继承标准输出的管道句柄,但如果父进程自身没有及时关闭持有的管道写端副本,管道的写端就会一直存在有效引用。此时操作系统会认为仍有数据可能被写入管道,ReadFile会持续阻塞等待,直到写端全部关闭才会返回0(表示无更多数据)。
你的多线程场景中,大概率存在管道写端句柄的管理疏漏——比如全局复用句柄、线程未及时关闭自身持有的写端副本,导致管道写端始终未被完全释放,触发ReadFile阻塞。
2. 所有线程到达ReadFile后阻塞解除的原因
当所有线程都执行到ReadFile步骤时,意味着所有子进程都已退出,且最后一个持有管道写端副本的线程也完成了子进程等待阶段,进入了管道读取流程——此时该线程会关闭自身持有的写端句柄,管道的所有写端引用被彻底释放。
一旦管道写端全部关闭,ReadFile会立即返回0,阻塞状态解除。从你的输出可见,线程1对应5秒任务,是最后一个完成WaitForSingleObject的线程,当它进入ReadFile时,所有管道写端都已被关闭,因此它的读取没有阻塞;而线程2、3之前一直卡在ReadFile等待写端关闭,直到线程1完成句柄清理,它们的阻塞才被解除。
内容的提问来源于stack exchange,提问作者ThE_-_BliZZarD

