简介:一份面向Visual C++开发者的Windows进程间通信技术文档,以“管道+多线程”为主线,分析在多任务系统内,进程如何借助共享内存式的管道实现数据交换与协作;相比剪贴板、DDE、OLE等传统通信手段,管道使用简便、无需复杂协议,适合需要掌握IPC底层原理的初中级C++工程师阅读。文档以VC++4.1环境下的父进程Parent与子进程Child通信实例为线索,详细列出CreatePipe创建管道、CreateProcess启动子进程、CreateThread建立读取线程、WriteFile写入数据、ReadFile读取数据、WaitForSingleObject等待线程结束等关键API的调用方式,并解释了管道缓冲区与安全属性的设置要点。在此基础上,还演示了父进程菜单项如何向子进程传递图形形状参数,让子进程在自身窗口中绘制对应图形,从而体现管道通信在实际协作任务中的完整编码链路,读者可以借鉴这套框架搭建自己的跨进程通信程序。包体仅含1个doc文件,压缩后61KB,内容紧凑不冗余,已有572人学习/下载,可作为管道通信项目启动时的直接参考。
1. 管道和线程做进程间通信:这个老方案至今仍是 VC 环境下的立身之本
很多人一提到进程间通信,脑子里先冒出来的是共享内存、Socket 或者消息队列,反而把管道这个最朴素的机制放在一边。实际上在 Windows 环境下,管道尤其是匿名管道,是父子进程之间交换数据成本最低的手段:不需要额外定义协议,不需要处理网络栈,只要把句柄通过创建进程时传进去,两边就能像读写文件一样通信。配合多线程把读操作从界面线程里拆出去,消息的收发就不会卡住窗口,这在当年 VC++ 4.1 那个年代是标准做法,放到现在用 Visual Studio 打开旧工程依然能跑。这篇笔记就是把一个实际的父进程 Parent 与子进程 Child 通信实例拆开,从管道创建、句柄继承、线程封装到参数传递一步步说清楚,让你拿到代码后自己能改、能复现、能排查。
这份资源的核心价值在于它把两个基础机制焊在了一起:一端是 CreatePipe 和 WriteFile 写数据,另一端是 CreateProcess 启动子进程后,用 CWinThread 派生的线程去 ReadFile 读数据。写成堡垒机那种复杂架构没有,但恰好是这个简单骨架,把进程创建时句柄表怎么继承、管道读写为什么必须放线程里、线程什么时候退出来结束通信这几个关键点全暴露出来了。适合正在看进程间通信、需要交课程设计,或者想把老代码逻辑迁移到新工程的人。
2. 父进程 Parent:管道创建、句柄去继承与子进程启动的三步操作
父进程这一侧承担两个职责:创建管道并且把读端句柄传给子进程,然后通过菜单事件往管道里写控制参数。代码逻辑看起来不长,但每一步都踩在 Win32 句柄继承的规则上,少做一步子进程就读不到数据。
2.1 先定义通信结构:两个进程之间传什么,用结构体定死
两个进程要交换信息,第一件事不是写代码而是定协议。匿名管道是字节流,没有消息边界,所以必须在发送端和接收端约定好每次写入的数据长度和含义。
// Global.h 共享变量头文件 typedef struct Figure { int iShape; // 图形控制参数 } FIGURE, *PFIGURE; #define ID_RECT 32771 #define ID_ELLIPSE 32772 #define ID_TERMINATE 32773这里定义了一个名为 FIGURE 的结构体,里面只有一个 int 类型的成员 iShape,用来表示图形的形状。三个宏定义当作菜单命令的消息 ID 使用,父进程通过菜单点击改变 figure.iShape 的值,子进程收到后根据这个值决定画矩形、画椭圆还是退出通信。
参数说明:结构体要跨进程传递,成员类型必须用固定长度的基础类型。这里只用了 int,在 Win32 下是 4 字节,两边编译环境一致就不会出问题。如果你要传更复杂的数据,比如字符串或者坐标数组,建议把结构体改成定长数组,或者额外传一个长度字段,避免管道字节流拆包时无法对齐。
2.2 创建管道与启动子进程:CreatePipe 后必须处理句柄继承
父进程的 OnLButtonDown 函数是整条链路的起点。鼠标左键按下后,创建管道、启动子进程、发送初始消息都发生在这里。
void CParentView::OnLButtonDown(UINT nFlags, CPoint point) { SECURITY_ATTRIBUTES sa; STARTUPINFO sui; PROCESS_INFORMATION pi; BOOL bTest; HANDLE hpipeRead; // 管道读句柄 // 填充安全性结构,使句柄可以被继承 sa.nLength = sizeof(SECURITY_ATTRIBUTES); sa.lpSecurityDescriptor = NULL; sa.bInheritHandle = TRUE; // 创建匿名管道 bTest = CreatePipe(&hpipeRead, &hpipeWrite, &sa, 0); if (!bTest) { MessageBox("CreatePipe failed!", NULL, MB_OK); return; } // 修改写句柄,使其不被继承 bTest = DuplicateHandle(GetCurrentProcess(), hpipeWrite, GetCurrentProcess(), NULL, 0, FALSE, DUPLICATE_SAME_ACCESS); if (!bTest) { MessageBox("Dup Handle failed!", NULL, MB_OK); CloseHandle(hpipeRead); CloseHandle(hpipeWrite); return; } CloseHandle(hpipeWrite); // 关闭原始写句柄 hpipeWrite = bTest; // 用不可继承的副本继续写 // 填充进程启动信息,指定标准输入为管道读端 memset(&sui, 0, sizeof(STARTUPINFO)); sui.cb = sizeof(STARTUPINFO); sui.dwFlags = STARTF_USESTDHANDLES; sui.hStdInput = hpipeRead; sui.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); sui.hStdError = GetStdHandle(STD_ERROR_HANDLE); // 创建子进程 Child.exe bTest = CreateProcess(NULL, "child.exe", NULL, NULL, TRUE, 0, NULL, NULL, &sui, &pi); if (!bTest) { MessageBox("CreateProcess failed!", NULL, MB_OK); CloseHandle(hpipeWrite); return; } hProcess = pi.hProcess; CloseHandle(pi.hThread); figure.iShape = ID_RECT; SendCommand(); CloseHandle(hpipeRead); // 父进程不再需要读端 CView::OnLButtonDown(nFlags, point); }这段代码的逻辑顺序是:先创建管道拿到读和写两个句柄,接着用 DuplicateHandle 把写句柄复制一份并把继承标志清掉,随后关闭原始写句柄,再用不可继承的副本作为父进程的写管道句柄,最后通过 STARTUPINFO 结构把读句柄挂到子进程的标准输入上,调用 CreateProcess 时传入 bInheritHandles 为 TRUE,子进程就能从标准输入拿到管道读端。
逻辑说明:管道创建出来时,读端和写端都可以被继承。如果不做 DuplicateHandle 这一步,子进程会把写端也继承过去。管道有一个特点,只有当所有写句柄都关闭后,读端 ReadFile 才会返回 FALSE 或 EOF。子进程如果持有写句柄而不关闭,即使父进程退出,读端也永远等不到数据结束,线程会一直阻塞在 ReadFile 上。
参数说明:CreatePipe 的最后一个参数 nSize 设为 0,表示使用系统默认的管道缓冲区大小,这在大多数场景下够用。DuplicateHandle 中的 DUPLICATE_SAME_ACCESS 表示副本和原始句柄拥有相同的访问权限,你不需要重新计算权限标志。CreateProcess 的第五个参数 bInheritHandles 必须为 TRUE,否则即使 STARTUPINFO 里指定了 hStdInput,子进程也无法继承句柄。
2.3 菜单命令与 SendCommand:WriteFile 写管道时要判断管道状态
菜单项 Rect、Ellipse、Terminate 分别触发不同的命令,但共同点是都调用 SendCommand 把当前 figure 结构体写入管道。
BOOL CParentView::SendCommand() { BOOL bTest; DWORD dwWritten; bTest = WriteFile(hpipeWrite, &figure, sizeof(FIGURE), &dwWritten, NULL); if (!bTest) { MessageBox("WriteFile failed!", NULL, MB_OK); } // 如果写入失败或收到终止命令,关闭进程和管道句柄 if ((!bTest) || (figure.iShape == ID_TERMINATE)) { CloseHandle(hProcess); hProcess = NULL; CloseHandle(hpipeWrite); } return bTest; }WriteFile 向管道写入 figure 结构体,sizeof(FIGURE) 指定写入字节数,dwWritten 返回实际写入的字节数。如果返回 FALSE,说明管道读端已经关闭,比如子进程已经退出,此时父进程应该清理自己的句柄。当用户选择 Terminate 时,父进程发送 ID_TERMINATE 消息后也主动关闭写句柄,表示通信结束。
这里有个细节值得注意:关闭 hpipeWrite 的时机。如果子进程已经退出,父进程还持有写句柄,这个句柄不会自动失效,但 WriteFile 会返回错误。所以 SendCommand 里把写入失败和收到终止命令两种条件放在一起判断,统一执行清理。
3. 子进程 Child:CWinThread 派生线程类,把 ReadFile 从界面线程里拆出来
子进程的难点不在读数据本身,而在于读管道这个操作是阻塞的。如果把它放在窗口过程或视图类里,管道里没数据时界面直接卡死。解决办法是启动时创建一个工作线程,专门负责循环读取管道,读到数据后再通知主窗口刷新。
3.1 为什么要用 CWinThread 派生类而不是 CreateThread 裸调
MFC 环境下创建线程有两条路:直接用 Win32 的 CreateThread,或者从 CWinThread 派生一个类。这篇代码选择的是后者。
// Thr.h 线程类头文件 class CThr : public CWinThread { public: LONG PipeThread(); // 线程循环入口 void DoRead(void); // 读取管道并处理数据 HANDLE hpipeRead; HANDLE hThread; DWORD dwThreadID; int iShape; BOOL bTerminate; };CWinThread 派生类的好处是把线程的创建、运行、退出封装进同一个类里,可以重写 InitInstance 做初始化,线程函数里还能访问类成员变量,数据共享比 CreateThread 的全局变量方式干净得多。类里声明了 hpipeRead 作为管道读句柄,bTerminate 作为退出标志,iShape 保存从管道读到的图形参数。
CWinThread::CreateThread在内部会调用AfxBeginThread的底层逻辑,它会初始化 MFC 内部状态并且调用 InitInstance 和 ExitInstance,这两步对使用 MFC 类的代码是必要的。如果是非 MFC 的 Win32 工程,用 CreateThread 也可以,但要自己管理线程退出和资源清理。
3.2 构造函数里获取句柄:标准输入不一定是管道
子进程被 CreateProcess 启动时,STARTUPINFO 里指定了 hStdInput 为管道读句柄,所以在子进程内部可以通过 GetStdHandle(STD_INPUT_HANDLE) 把这个句柄拿回来。
// Thr.cpp 线程类实现文件 CThr::CThr() { HWND hwnd = GetActiveWindow(); // 获取标准输入句柄,此时它指向父进程创建的管道读端 hpipeRead = GetStdHandle(STD_INPUT_HANDLE); if (hpipeRead == INVALID_HANDLE_VALUE) ::MessageBox(hwnd, "Invalid Handle!", NULL, MB_OK); }构造函数里做了句柄获取和有效性检查。注意 GetStdHandle 返回 INVALID_HANDLE_VALUE 并不常见,但如果子进程不是通过 CreateProcess 带 STARTF_USESTDHANDLES 方式启动的,标准输入可能是控制台的输入缓冲,此时往这个句柄上做 ReadFile 行为会完全不同。所以这个检查不能省。
提示:如果子进程是双击 exe 直接运行的,hpipeRead 不会指向管道,而是指向空的控制台输入流,ReadFile 会一直等待用户从键盘输入。调试时要注意区分启动方式。
3.3 线程主循环与 DoRead:阻塞读管道,按消息类型决定行为
InitInstance 里设置线程优先级并启动线程主循环,这是线程的入口。
BOOL CThr::InitInstance() { bTerminate = FALSE; // 设置线程优先级为低于正常,避免影响界面响应 SetThreadPriority(THREAD_PRIORITY_BELOW_NORMAL); ResumeThread(); PipeThread(); return TRUE; } LONG CThr::PipeThread() { // 循环读取直到收到退出标志 while (!bTerminate) { DoRead(); } return 0L; }这里的 InitInstance 和常见的 MFC 线程写法略有不同:它没有直接返回 FALSE 退出线程,而是调用了 PipeThread 并让它进入循环。PipeThread 是一个普通成员函数,循环里不断调用 DoRead,DoRead 里的 ReadFile 是阻塞调用,管道里没有数据时线程会挂起等待,不占用 CPU。
void CThr::DoRead() { FIGURE Figure; DWORD dwRead; BOOL bTest; // 阻塞读管道 bTest = ReadFile(hpipeRead, &Figure, sizeof(Figure), &dwRead, NULL); if (bTest) { if (Figure.iShape == ID_TERMINATE) { bTerminate = TRUE; // 收到终止命令,退出循环 } else { iShape = Figure.iShape; // 保存图形参数 HWND hwndMain = GetActiveWindow(); InvalidateRect(hwndMain, NULL, TRUE); UpdateWindow(hwndMain); // 刷新主窗口 } } else { // 管道写端全部关闭,读失败,退出 bTerminate = TRUE; } return; }DoRead 的逻辑很直观:先调用 ReadFile 阻塞等待数据,读成功就判断消息类型,是终止命令就置位 bTerminate 结束循环,是图形参数就保存到成员变量 iShape 并刷新窗口。读失败说明管道写端已经全部关闭,这种情况下继续循环没有意义,直接把 bTerminate 置为 TRUE 退出。
参数说明:ReadFile 的第三个参数必须和父进程 WriteFile 的写入字节数保持一致,这里两边都是 sizeof(FIGURE)。如果父进程写入的是 4 字节而子进程期望读 8 字节,ReadFile 会一直阻塞等待数据凑满,导致通信卡死。这一点在修改结构体时尤其要注意。
3.4 视图类中启动线程:CreateThread 的时机必须在窗口创建之前
子进程的 ChildView 是窗口的核心类,线程对象的生命周期和它绑定。
// Childview.cpp 视类实现文件 CThr* m_pThr; // 线程对象指针 CChildView::CChildView() { m_pThr = new CThr; // 创建线程对象,构造函数里获取管道句柄 } CChildView::~CChildView() { delete m_pThr; // 删除线程对象 } BOOL CChildView::PreCreateWindow(CREATESTRUCT& cs) { m_pThr->CreateThread(); // 启动线程 return CView::PreCreateWindow(cs); }线程的创建被放在 PreCreateWindow 中,这一步发生在窗口真正创建之前。为什么这样做:CThr 的构造函数里调用了 GetActiveWindow,如果窗口尚未创建,这个调用拿到的窗口句柄可能不准,但管道句柄的获取不受影响。更关键的是,InitInstance 中的 PipeThread 启动后,可能会立刻收到父进程写入的数据并触发 InvalidateRect,如果此时窗口还没创建,刷新操作会落空。把 CreateThread 放在 PreCreateWindow 中,能保证窗口资源基本就绪后再启动读取循环。
绘制部分在 OnDraw 中根据 m_pThr->iShape 的值决定画矩形还是椭圆,这个不再展开,属于标准的 MFC 绘图逻辑。
4. 句柄继承与线程退出:把两个进程通信的全链路串起来看
前面分开讲了父进程和子进程各自的实现,但真正理解这个架构,需要把两边的句柄关系和线程状态串在一起看。这一章把整个通信链路从头到尾走一遍,说明每个句柄在哪个进程、哪个阶段发挥作用。
4.1 管道句柄在不同进程中的分布情况
CreatePipe 创建管道后,系统返回两个句柄:hpipeRead 和 hpipeWrite。这两个句柄都位于父进程的句柄表里。经过 DuplicateHandle 操作后,父进程持有的是不可继承的 hpipeWrite 副本和 hpipeRead。CreateProcess 时,由于 bInheritHandles 为 TRUE,子进程继承了 hpipeRead,并把它作为标准输入句柄。
两个进程的句柄分布可以用一张表说清楚:
| 句柄 | 父进程状态 | 子进程状态 | 作用 |
|---|---|---|---|
| hpipeRead | 创建后持有,启动子进程后关闭 | 继承后作为标准输入 | 子进程从中读取数据 |
| hpipeWrite(原始) | DuplicateHandle 后关闭 | 未继承 | 不再存在 |
| hpipeWrite(副本) | 父进程持有 | 未继承 | 父进程写入数据 |
父进程在 CreateProcess 之后调用了 CloseHandle(hpipeRead),这是因为父进程只需要写数据,读端留在手里没有意义。更重要的是,如果父进程不关闭读端,管道读端不会真正转移到子进程侧,这在某些管道实现下会导致读写双方对 EOF 状态判断不一致。
提示:在匿名管道里,管道生命周期的结束条件是所有写句柄被关闭。父进程持有写句柄副本,子进程持有读句柄,任何一边退出或关闭句柄,另一边的 ReadFile 或 WriteFile 都会感知到变化。
4.2 写数据时管道缓冲区的行为
父进程每次 WriteFile 写入一个 FIGURE 结构体,这个结构体只有 4 字节。匿名管道的默认缓冲区大小一般是 4096 字节以上,所以数据写入后不会立即触发子进程的 ReadFile 返回,而是要等子进程主动调用 ReadFile 才会从缓冲区取走。
这里有一个在实际调试中很容易误解的点:管道是字节流,没有消息边界。父进程写入 10 次 FIGURE,子进程不一定按每次 4 字节来消费,可能一次 ReadFile 就把缓冲区里攒下的 40 字节都读走。这个例子里因为父子进程读写节奏匹配,没有出现拆包问题,但如果你的场景里要传输大量数据,必须自己定义消息格式,在结构体前加长度字段或者在数据尾部加分隔符。
4.3 子进程线程退出的完整路径
通信结束有两种路径:一是用户点击 Terminate,父进程发送 ID_TERMINATE 并关闭写句柄,子进程线程读到终止消息后将 bTerminate 置为 TRUE,退出循环;二是父进程直接退出,写句柄被系统关闭,子进程的 ReadFile 返回 FALSE,同样将 bTerminate 置为 TRUE 退出。
不管哪条路径,最终子进程线程都会退出 PipeThread 循环并结束线程。这里有一个关键点:bTerminate 变量是在线程内部读取的,没有跨线程竞争问题,不需要加锁。但如果以后你打算在外部强制终止线程,就不能只靠这个标志位,还需要配合 WaitForSingleObject 等待线程真正结束。
5. 避坑手册:句柄继承、线程阻塞与消息丢失的典型问题
这个例子代码简洁,但真拿去跑或者改写成自己的工程,会遇到不少让你挠头的问题。下面几条是实践中最容易踩的坑,每条都按现象、原因、解决的顺序说清楚。
5.1 子进程读不到数据,界面永远不刷新
现象:父进程创建管道并启动子进程后,子进程窗口正常出现,但无论怎么点击菜单,子进程画布上始终是空白,也没有报错。
原因:最常见的是 DuplicateHandle 那步被省略或写错。如果原始 hpipeWrite 没有被关闭,而是直接把它传给父进程使用,子进程在继承时会把读端和写端都继承过去。子进程的 CThr 构造函数里通过 GetStdHandle 拿到的句柄虽然是读端,但子进程的句柄表里同时也保留了一份写句柄,导致整个管道在子进程内部就形成了自持状态,外部写句柄关闭后管道仍无法到达 EOF,但这通常不会影响正常 WriteFile 写入。真正影响数据到达的是另一类问题——STARTUPINFO 里没有设置 STARTF_USESTDHANDLES,导致系统忽略了 hStdInput,子进程拿到的标准输入句柄指向了控制台输入,ReadFile 阻塞的不是管道而是键盘输入。
解决:检查三个地方。第一,CreatePipe 的 SECURITY_ATTRIBUTES 中 bInheritHandle 必须为 TRUE;第二,CreateProcess 的 bInheritHandles 参数必须为 TRUE;第三,STARTUPINFO 中 dwFlags 必须包含 STARTF_USESTDHANDLES,并且 hStdInput 指向管道读端。如果三个条件都满足还不行,在子进程 CThr 构造函数里加一句 GetHandleInformation 检查句柄类型,确认 hpipeRead 确实是管道句柄。
5.2 Terminate 消息发了,但子进程窗口不退出
现象:点击 Terminate 菜单后,父进程侧句柄正常关闭,但子进程窗口仍停留在屏幕上,线程没有退出。
原因:看 DoRead 的代码,ReadFile 读到了 ID_TERMINATE 消息后把 bTerminate 置为 TRUE,PipeThread 退出循环,线程结束。但窗口本身不归线程管,线程退出后窗口依然存在。很多第一次接触这个例子的人以为发送 Terminate 应该关掉整个子进程窗口,实际上这段代码只是结束通信线程,窗口进程还活着。
解决:如果希望子进程窗口也退出,需要在子进程中额外处理,比如收到 ID_TERMINATE 后向主窗口发送 WM_CLOSE 消息,或者在线程退出后调用 PostQuitMessage。代码里可以这样改:在 DoRead 中检测到 ID_TERMINATE 时,bTerminate = TRUE 之外再调用 AfxGetMainWnd()->PostMessage(WM_CLOSE),让窗口关闭流程走一遍。
5.3 ReadFile 阻塞在管道上,线程退不出来
现象:子进程窗口已经关闭,但任务管理器里还能看到子进程的 exe 进程残留,或者调试时发现线程卡在 ReadFile 调用上始终不返回。
原因:ReadFile 在管道读端没有数据且写句柄仍然存在的情况下会一直阻塞。如果父进程持有的写句柄没有被正确关闭——比如没有走 SendCommand 的清理逻辑,或者父进程本身被异常终止——管道写端没有完全关闭,子进程的 ReadFile 就永远不会返回 FALSE,线程无法退出。
解决:父进程在发送 ID_TERMINATE 后一定要执行 CloseHandle(hpipeWrite),让管道写端彻底关闭。另外,子进程里可以考虑给 ReadFile 设置超时,或者改用 PeekNamedPipe 先查询管道状态再决定是否阻塞读取。对于这个示例来说,最实用的做法是在子进程收到 ID_TERMINATE 后主动关闭自己的 hpipeRead 句柄,这样即使父进程侧写句柄没关干净,子进程侧也能通过句柄关闭打破 ReadFile 的阻塞。
5.4 图形参数传过去了,但画出来的永远是矩形
现象:子进程能收到数据,窗口会刷新,但无论父进程选择 Ellipse 还是 Rect,子进程只画矩形。
原因:这个问题大概率出在菜单资源的 ID 值不一致上。Parent 的菜单项 ID 和在 Global.h 里定义的 ID_RECT、ID_ELLIPSE 是两套数字,OnRect 和 OnEllipse 函数里把 figure.iShape 设置成 ID_RECT 或 ID_ELLIPSE,但如果菜单资源文件 .rc 中的实际 ID 值和图形式常量不一致,编译器不会报错,只是子进程收到的值不是预期值。
解决:检查 Parentview.cpp 中 OnRect 和 OnEllipse 里的赋值是否和 Global.h 中的宏定义一致,再检查子进程 OnDraw 中比较的分支是否使用同一个宏。最稳妥的方式是让菜单资源 ID 直接使用 Global.h 里的宏值,而不是在资源编辑器里另建一套 ID,这样可以避免两边数值错位。
5.5 线程对象 delete 时程序崩溃
现象:子进程窗口关闭时,程序在 CChildView 析构函数里执行 delete m_pThr 时崩溃,或者报堆损坏错误。
原因:CThr 的 CWinThread 对象生命周期和线程执行周期不完全同步。当用户关闭窗口时,视图类析构函数销毁 m_pThr,但如果线程还阻塞在 ReadFile 上没有退出,此时直接 delete CWinThread 对象,MFC 内部的线程状态尚未清理干净,会导致非法访问。
解决:在 delete 之前先确保线程已经退出。常见做法是给 CThr 增加一个终止机制,先设置 bTerminate 并关闭 hpipeRead 句柄让 ReadFile 返回,再调用 WaitForSingleObject(m_pThr->m_hThread, INFINITE) 等待线程完全结束,之后才执行 delete。注意 m_hThread 是 CWinThread 的公开成员,可以在外部访问。
6. 验证这个通信链路:加一个消息计数器和内存映射的替代方案
代码跑通后,怎么确认通信是可靠的,而不是碰巧能画出图形?把验证方法写清楚,比反复调试界面更有价值。
在父进程中加一个计数器成员变量 UINT m_nSentCount,每次 SendCommand 成功后递增并输出到窗口标题栏。在子进程的 DoRead 中也维护一个计数,代表收到消息的总数,每次读取成功后把它显示在子进程窗口的左上角。两个计数保持一致,说明没有丢消息。
父进程侧在 SendCommand 里加一行:
m_nSentCount++; SetWindowText(GetParent()->GetSafeHwnd(), ...);子进程侧在 DoRead 中读取成功后:
m_nRecvCount++; // 可以在窗口上绘制或者用 TRACE 输出如果两个计数不一致,说明管道数据在某个环节丢失或者写读节奏不匹配。通常原因是父进程写太快而子进程读太慢,管道缓冲区溢出,这时需要调整写入频率或者增大管道缓冲区。
另一个更底层的验证方法是利用管道句柄的可等待属性。ReadFile 在匿名管道上的阻塞是操作系统的内核同步机制,你可以把 hpipeRead 交给 WaitForSingleObject 等待数据到达,但它和事件对象不同的是,管道句柄变为 signaled 表示缓冲区有数据,但不代表读取一定能成功。这个特性对调试很有帮助,可以在子进程线程加一个分支:
DWORD dwWait = WaitForSingleObject(hpipeRead, 1000); if (dwWait == WAIT_OBJECT_0) { bTest = ReadFile(...); }这样就不怕 ReadFile 无限期阻塞,每次等待 1 秒后即使没有数据也会返回到循环体,配合计数器可以定位到阻塞发生的具体位置。
如果要扩展到更灵活的场景,比如两个完全独立的进程通信,匿名管道就力不从心了,这时要换成命名管道。命名管道可以通过管道名称让不同进程打开同一个管道两端,还支持消息模式,每条消息保持边界,不用自己手工拆包。代码改动也不大:把 CreatePipe 换成 CreateNamedPipe,父进程用 CreateFile 连接管道名,子进程用 ConnectNamedPipe 等待连接。发送和接收端的逻辑基本不变。
我自己的习惯是先用这个匿名管道的例程把进程间通信的完整链路走通,再根据实际需要往命名管道迁移。毕竟从控制台程序到 MFC 窗口程序,管道的读写逻辑几乎不受影响,换掉创建和连接环节就能复用大部分代码。如果你手里的工程也遇到写一句读一句的进程通信需求,这个例子里的线程模型可以直接照搬,但记住把线程退出机制做干净,不然开发和发布版本都会遇到句柄泄漏或僵尸进程。
那次把 Terminate 菜单改崩之后,我每次在 MFC 里销毁线程对象前都强制走一遍:置位退出标志、关闭阻塞句柄、等待线程结束、再做清理,再也没出过崩溃问题。希望这份拆解能帮你少走这段弯路。
本文还有配套的精品资源,点击获取