管道与多线程:VC环境下父子进程通信实例拆解

发布时间:2026/10/6 15:04:50
管道与多线程:VC环境下父子进程通信实例拆解 简介一份面向Visual C开发者的Windows进程间通信技术文档以“管道多线程”为主线分析在多任务系统内进程如何借助共享内存式的管道实现数据交换与协作相比剪贴板、DDE、OLE等传统通信手段管道使用简便、无需复杂协议适合需要掌握IPC底层原理的初中级C工程师阅读。文档以VC4.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 菜单命令与 SendCommandWriteFile 写管道时要判断管道状态菜单项 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. 子进程 ChildCWinThread 派生线程类把 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 是一个普通成员函数循环里不断调用 DoReadDoRead 里的 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 置为 TRUEPipeThread 退出循环线程结束。但窗口本身不归线程管线程退出后窗口依然存在。很多第一次接触这个例子的人以为发送 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 里销毁线程对象前都强制走一遍置位退出标志、关闭阻塞句柄、等待线程结束、再做清理再也没出过崩溃问题。希望这份拆解能帮你少走这段弯路。本文还有配套的精品资源点击获取