做客户端/桌面端架构的同学,多进程是绕不开的话题:主进程 + 辅助进程,怎么通信?后端微服务之间走 RPC,但客户端进程间不能随便上 RPC(重、要起服务、要端口)。本文复盘 yyzTools(一套 Windows 桌面效率工具集)的多进程 IPC 选型,看它如何用 4 种不同量级的通信方式(命令行参数 / 共享配置 / 窗口消息 / stdout)搭起一套多进程编排,以及每种方式的适用边界。适合做客户端架构、多进程编排、Windows 系统编程的读者参考。
一、客户端多进程为什么不能照搬微服务 IPC
微服务间通信用 RPC(gRPC/Thrift)或消息队列,理所当然。但客户端多进程不能照搬:
| 微服务 IPC | 客户端多进程的问题 |
|---|---|
| gRPC | 要起 server、占端口、客户端进程又不是长期服务 |
| 消息队列 | 要 broker、要部署、桌面端没法带 |
| REST | 要 HTTP server、要序列化、太重 |
| 共享内存 | 可以,但要同步原语、要管生命周期 |
客户端多进程的特点是:进程是按需启动的、短生命周期的、不需要常驻服务的。拉起一个辅助进程干完活就退出,不值得为它起一个 RPC server。
yyzTools 的多进程是「主进程常驻 + 辅助进程按需启动用完即走」。这套模型下 IPC 选型要轻、要适合短生命周期、要利用 OS 已有的机制。
二、yyzTools 的 4 种 IPC 量级
yyzTools 用了 4 种不同量级的进程间通信,按「从轻到重」排:
flowchart LR
Main["yyzTools.exe<br/>主进程(常驻)"]
subgraph aux["辅助进程 · 按需启动 · 用完即走"]
Browser["yyzBrowser.exe"]
Cli["openssl / 7z / ffmpeg"]
WP["yyzWallpaper.exe"]
end
Main -->|"① 命令行参数:--url=... --size=WxH<br/>启动配置,一次性单向"| Browser
Main -->|"② 拉起 + 参数透传"| Cli
Cli -->|"② stdout:结果回传(请求-响应)"| Main
Main <-->|"③ 共享配置/缓存<br/>倒计时缓存跨进程读写"| WP
Main -->|"④ 窗口消息<br/>WM_SETTINGCHANGE 事件转发"| WP
量级一:命令行参数(启动时一次性传递)
主进程拉起辅助进程时,通过 CreateProcess 的命令行传初始参数。
最典型的:浏览器进程 yyzBrowser.exe --url=... --size=WxH。主进程要把「打开什么网址、窗口多大」告诉浏览器进程,直接拼到命令行,浏览器进程解析完就跑。
适用场景:启动时的初始配置,一次性、单向、不需响应。这是最轻的 IPC,零状态、零开销。
量级二:stdout(结果回传)
子进程干完活,结果通过 stdout 回传,主进程读。
这套在 [[02-子进程透传架构]] 里详述:主进程拉起 openssl/7z/ffmpeg 等子进程,把算法名/参数拼命令行透传,子进程把结果输出到 stdout,主进程读 stdout 包成 JSON 回业务层。
适用场景:子进程是「拉起 -> 跑完 -> 退出」的短生命周期,结果一次性回传。这是「请求-响应」模式的最轻实现,不用长连接。
量级三:共享配置/缓存(跨进程持久化读写)
有些数据要跨进程共享读写,比如倒计时缓存。yyzTools 的壁纸进程持有独立 native 能力做这类跨进程读写,走文件或共享内存。
适用场景:状态要持久化、跨进程共享、不要求实时强一致。文件/共享内存比消息通信重,但适合「共享状态」而非「传消息」。
量级四:窗口消息(系统事件转发)
Windows 的 PostMessage / PostThreadMessage 在进程间转发系统事件。yyzTools 的典型场景:壁纸进程要响应系统主题变化(WM_SETTINGCHANGE),主进程收到后转发给壁纸进程。
适用场景:Windows 系统事件的进程间转发,要利用 Win32 消息泵。这是 Windows 特有的 IPC,轻量、事件驱动、适合系统通知。
三、4 种量级的适用边界
这 4 种不是替代关系,是不同场景用不同量级:
| 量级 | 模式 | 生命周期 | 双向 | 典型场景 |
|---|---|---|---|---|
| 命令行参数 | 启动配置 | 一次性 | 否 | 拉起浏览器带 url/size |
| stdout | 请求-响应 | 短 | 半(结果回传) | openssl/7z 干活回结果 |
| 共享配置/缓存 | 共享状态 | 持久 | 是 | 倒计时缓存跨进程读写 |
| 窗口消息 | 事件转发 | 事件驱动 | 是 | WM_SETTINGCHANGE 转壁纸进程 |
选型原则:
初始配置 -> 命令行参数(最轻,零状态)
干完回结果 -> stdout(短生命周期的请求-响应)
共享持久状态 -> 文件/共享内存(要长期保存)
系统事件转发 -> 窗口消息(Windows 特有)
都不用 RPC。这套多进程里没有一个是 RPC--因为没有一个辅助进程是「长期服务别人」的角色。都是「被拉起干完退出」,用 RPC 是杀鸡用牛刀。
四、一个关键设计:进程依赖单向
yyzTools 多进程架构里有一条硬约束:进程依赖单向。
主进程拉起辅助进程、给它们派活,但辅助进程不能反向依赖主进程。具体表现:某个辅助进程(比如用于单元测试的桩进程)没有 GetCmdManager() 这类主进程才有的能力。如果代码在辅助进程里调用了主进程才有的方法,会崩。
所以有一层约束:辅助进程需要的能力,要么自己带,要么通过显式的跨进程通信拿,不假设主进程在场。主进程和辅助进程的编排逻辑放在更上层(比如配置流程的回调里),不塞进进程自身。
flowchart TD
Main["主进程 yyzTools.exe<br/>常驻,持有全部 Manager 与编排逻辑"]
subgraph aux["辅助进程:不假设主进程在场"]
Browser["yyzBrowser.exe"]
WP["yyzWallpaper.exe"]
Tests["tests_app.exe<br/>单元测试桩,无 GetCmdManager()"]
end
Main -->|"拉起 · 派活"| Browser
Main -->|"拉起 · 窗口消息"| WP
Browser -.->|"✗ 反向依赖主进程(会崩)"| Main
Tests -.->|"✗ 假设主进程在场(无法独立测试)"| Main
linkStyle 2 stroke:#c0392b
linkStyle 3 stroke:#c0392b
这条约束的意义是进程可独立测试 + 可独立运行。辅助进程不假设主进程在场,就能脱离主进程跑单元测试。这对可测试性是关键。
五、语言切换的多进程协调(一个实战案例)
yyzTools 切换界面语言是个典型的多进程协调场景。调用链:
ConfigProcess::LanguageCallback
-> ModuleManager::LanguageChanged() // 同步 WebModule 窗口标题 + 重载页面
-> CmdManager::UpdateModuleTexts() // 刷新命令面板文案与拼音索引
sequenceDiagram
autonumber
participant CP as ConfigProcess
participant MM as ModuleManager
participant CM as CmdManager
CP->>MM: LanguageChanged()
Note right of MM: 同步 WebModule 窗口标题 + 重载页面
MM-->>CP: 完成
CP->>CM: UpdateModuleTexts()
Note right of CM: 刷新命令面板文案与拼音索引
CM-->>CP: 完成
两步都要,漏掉第二步命令面板会一直显示旧语言直到重启。
这里的工程要点:
模块文案一次性读入内存(
ModuleTextMap m_moduleTexts,key 为模块 id)。切换语言只是换 key 取值,不再读盘。别在语言切换路径上重新解析 .zenmod,那会把 379 个文件连 base64 图标一起重读。ModuleManager不反向依赖CmdManager:unit_tests用的是自己的Application桩,没有GetCmdManager(),这两步的编排放在ConfigProcess里,不塞进ModuleManager。
这是「进程依赖单向」约束在具体功能里的体现:编排逻辑放上层,进程自身不假设跨进程依赖。
六、和微服务 IPC 的位置对比
| 维度 | 客户端多进程 IPC(yyzTools) | 微服务 IPC |
|---|---|---|
| 进程生命周期 | 短(按需启动用完即走) | 长(常驻服务) |
| 通信方式 | 命令行/stdout/文件/窗口消息 | RPC/MQ |
| 状态 | 无状态或文件共享 | 有状态 + 服务发现 |
| 端口占用 | 无 | 有 |
| 序列化 | stdout 文本 / 命令行 | Protobuf/JSON over network |
| 适合场景 | 单机多进程编排 | 跨机分布式 |
客户端多进程 IPC 的核心是**「够轻、够短、够无状态」**。进程不是服务,不长期待命接请求,而是被拉起干完就走。这决定了 IPC 要用 OS 已有机制(命令行/stdout/窗口消息/文件),不引入网络和序列化框架。
七、几个工程坑
7.1 stdout 的结构化契约
子进程的 stdout 默认给人看,不是给程序解析。主进程读 stdout 要有稳定契约(最好 JSON),否则 CLI 升级改了输出格式就解析崩。yyzTools 的做法是 C++ 层负责把 CLI 原始输出规整成统一 JSON 给业务层。
7.2 窗口消息的线程亲和
PostMessage 到窗口要保证目标窗口的消息泵在跑。辅助进程刚拉起、消息泵还没就绪时发消息会丢。要等目标进程就绪后再发,或用 PostThreadMessage 到特定线程。
7.3 共享状态的并发
文件/共享内存的跨进程读写要处理并发--读写锁、原子操作。比进程内并发复杂,因为不能用进程内锁。yyzTools 的倒计时缓存这类共享状态要显式管同步。
7.4 进程生命周期管理
按需启动的辅助进程要管超时、取消、僵尸进程。主进程拉起子进程后不能放任不管--子进程 hang 了主进程要能杀掉它。这套生命周期管理是子进程透传架构的额外成本。
八、小结
客户端多进程的 IPC 不该照搬微服务的 RPC。yyzTools 用 4 种不同量级的通信方式搭起一套多进程编排:
命令行参数:启动配置一次性传递
stdout:短生命周期请求-响应
共享配置/缓存:持久共享状态
窗口消息:Windows 系统事件转发
核心原则是**「够轻、够短、够无状态」**,用 OS 已有机制,不引入网络和序列化框架。配合「进程依赖单向」约束保证可测试性。
对做客户端/桌面端多进程架构的,这套选型思路值得参考:先问「这个通信是一次性配置、还是请求-响应、还是共享状态、还是事件转发」,按场景选量级,别一上来就 RPC。进程不是服务,IPC 该轻则轻。
项目地址:yyztools.com
进程模型:主进程常驻 + 辅助进程按需启动 · 4 量级 IPC · 永久免费