- A+
背景
进程间通讯属于老生常谈的话题,可能已经有很多的通信示例代码,但在实际使用中需要做的东西还比较多。例如协议定制、消息收发、进程管理等都需要实现,进阶需求可能还需要实现回调函数、取消等。
个人在工作中恰好遇到了相关需求,然后在网上搜了一下,只找到两个.net下多进程相关的库(有可能是搜索方式不对)
https://github.com/tmds/Tmds.ExecFunction
https://github.com/CyAScott/AppDomainAlternative/
但并不满足需求,不能进程复用,或者不能方便的进行回调、取消。于是乎进行手写相关代码。
在手写了两三个相似的项目后,觉悟到重复的劳动是可以进一步省略的,于是进行了封装,产生了该项目 - Juxtapose 基于 SourceGenerator
的硬编码 .Net
多进程
运行库。只需要几行额外的代码就能使现有代码(*)支持多进程运行,进程复用,支持Linux
、Windows
,支持回调委托
和CancellationToken
,支持调试子进程(*)。无显式的运行时反射调用和动态构造类。
好处很多,那么代价是什么呢?
- 简单的一次方法调用将会经历数据传输以及至少四次序列化或反序列化;对比进程内方法调用来说是巨大的损耗。
- 方法参数只能是简单的数据类型(委托和CancellationToken除外,它们进行了特化处理),不能传递有行为的对象。
- 跨进程的异常信息将被包裹,需要自行解析;
- 调用堆栈将会变复杂;
综上,使用时需要能够接受和处理上述情况。
Juxtapose早期版本已经在生产环境进行了长时间的使用验证,但新的Release版本暂时还没有;
相关技术
SourceGenerator
源生成器 - SourceGenerator
已经推出有一段时间了,参见官方公告 Introducing C# Source Generators。
官方简介:
源生成器是一项 C# 编译器功能,使 C# 开发人员能够在编译用户代码时进行检查,并动态生成新的 C# 源文件,以添加到用户的编译中。 通过这种方式,你的代码可以在编译过程中运行并检查你的程序以生成与其余代码一起编译的其他源文件。
使用SourceGenerator
可以在编译时生成代码,避免运行时反射,提高运行效率,使FullAOT成为可能,代码运行逻辑也更加直观。不在此处进行过多描述,已经有不少的文章对其进行了介绍,可以随便搜索到,官方也有完善的示例代码。
相关资源:
进程间通讯
通讯方式很多,可以参考文章 c#多进程通讯,今天,它来了。
Juxtapose默认实现了命名管道通信(有需求可以自行实现其他方式进行替换),默认使用命名管道的主要原因是它由.net原生集成、支持双工通信、跨平台。
使用场景
以下为实际使用场景,更多场景请自行结合实际需求。
- 某组件不允许在一个进程内创建多个实例,无法并发;
- 某组件使用了过多内存、内存泄漏、内存释放不及时,导致进程被杀、Pod重启、服务中断;
- 某组件执行某耗时操作时,不支持取消操作,且占用大量CPU/内存资源,影响程序运行;
- 某组件直接操作内存,会偶发性出现非法访问,导致进程退出;
Nuget包介绍
1. Intro
基于 SourceGenerator
的硬编码 .Net
多进程
运行库。
包列表
名称 | 介绍 |
---|---|
Juxtapose | 运行时库,封装通用的功能 |
Juxtapose.SourceGenerator | 源代码生成器,用于代码生成 |
Juxtapose.VsDebugger | VisualStudio调试附加包,用于子进程调试 |
2. Features
- 可以为
类型
、接口
、静态类
生成代理,无需手动编写RPC相关代码,即可多进程
运行; - 编译时生成所有代码,运行时无显式的反射调用和动态构造;
- 支持
委托
和CancellationToken
类型的方法参数(其余类型未特殊处理,将会进行序列化,目前回调委托
不支持嵌套和CancellationToken
); - 支持
Linux
、Windows
(其它未测试); - 支持调试子进程(
Windows
&&VisualStudio
Only);
注意事项
- 目前参数不支持定义为父类型,实际传递子类型,序列化时将会按照定义的类型进行序列化和反序列化,会导致具体类型丢失;
- 目前所有的参数都不应该在方法完成后进行保留,
CancellationToken
、委托
等在方法完成后会被释放;
3. Requirement
- .Net5.0+(其它版本没有尝试过)
4. 使用方法
4.1 引用包
<ItemGroup> <PackageReference Include="Juxtapose" Version="1.0.0" /> <PackageReference Include="Juxtapose.SourceGenerator" Version="1.0.0" /> </ItemGroup>
4.2 建立上下文
4.2.1 创建上下文类型,并使用 [Illusion]
特性指定要生成的类型
[Illusion(typeof(Greeter), typeof(IGreeter), "Juxtapose.Test.GreeterAsIGreeterIllusion")] public partial class GreeterJuxtaposeContext : JuxtaposeContext { }
示例代码将为Greeter
生成IGreeter
接口的代理类型Juxtapose.Test.GreeterAsIGreeterIllusion
;
Note!!!
- 必须继承
JuxtaposeContext
; - 必须标记
partial
关键字;
4.2.2 [Illusion]
的多种用法
- 直接为类型生成代理,如下示例生成
Juxtapose.Test.GreeterIllusion
类型,且不继承接口(静态类型相同用法)
[Illusion(typeof(Greeter))]
- 为类型生成代理,并继承指定接口,如下示例生成
Juxtapose.Test.GreeterAsIGreeterIllusion
类型且继承IGreeter
接口
[Illusion(typeof(Greeter), typeof(IGreeter))]
- 生成类型,并指定类型名称,如下示例生成
Juxtapose.Test.HelloGreeter
类型
[Illusion(typeof(Greeter), generatedTypeName: "Juxtapose.Test.HelloGreeter")]
- 生成从IoC容器获取的接口代理类型,如下示例生成
Juxtapose.Test.IGreeterIllusionFromIoCContainer
类型(此时Context类需要实现IIoCContainerProvider
接口,并提供有效的IServiceProvider
)
[Illusion(typeof(IGreeterFromServiceProvider), generatedTypeName: "Juxtapose.Test.IGreeterIllusionFromIoCContainer", fromIoCContainer: true)]
4.3 添加入口点
在Main
方法开始处添加入口点代码,并使用指定上下文
await JuxtaposeEntryPoint.TryAsEndpointAsync(args, GreeterJuxtaposeContext.SharedInstance);
到此已完成开发,创建类型Juxtapose.Test.GreeterAsIGreeterIllusion
的对象,并调用其方法,其实际逻辑将在子进程中运行;
5. 调试子进程(Windows
&&VisualStudio
Only)
5.1 引用调试包
在Host项目文件中添加包引用
<ItemGroup Condition="'$(Configuration)' == 'Debug'"> <PackageReference Include="Juxtapose.VsDebugger" Version="1.0.0" /> </ItemGroup>
建议和示例代码一样,添加条件引用,只在Debug
环境下引用调试包
5.2 添加入口点
在Main
方法开始处添加调试入口点代码
JuxtaposeDebuggerAttacher.TryAttachToParent(args);
在代码中打上断点,运行时将会正确命中断点(只在 VisualStudio2022 17.0.5
&& Win11 21TH2
中进行了测试,理论上是通用)
6. 工作逻辑
SourceGenerator
在编译时生成代理类型,封装通信消息。在创建代理类型对象时,会自动创建子进程,并在子进程中创建目标类型的对象,使用命名管道进行进程间通信,使用System.Text.Json
进行消息的序列化与反序列化。更多技术细节可以参考源代码。
链接
源代码: https://github.com/stratosblue/juxtapose
Nuget: https://www.nuget.org/packages/Juxtapose