使用 JointCode.Shuttle 改进跨 AppDomain 通信






4.89/5 (3投票s)
JointCode.Shuttle 是一个快速、灵活且易于使用的面向服务的框架,用于跨 AppDomain 通信。它是运行时库提供的 MarshalByrefObject 的替代品。
引言
本文比较了两种用于进行跨 AppDomain 调用的不同技术的性能。
背景
跨 AppDomain 调用可能非常糟糕。测试表明,跨 AppDomain 方法调用比同一 AppDomain 中的正常方法调用慢数百到数千倍。
现在我们知道,当一个对象从一个执行边界(AppDomain)传递到另一个执行边界时,需要将其转换为可以传输的形式;然后当它到达时,它会被重建并在内存中转换回原始形式。这个过程被称为“封送”。
例如,在 .NET Remoting 中,一个 MarshalByrefObject 派生的对象将被打包成一个可序列化的 ObjRef 实例,这就是类型名称中“ByRef”的含义,它指的是 ObjRef 的形式。
通过封送,会通过诸如反射、安全检查、序列化/反序列化等方式产生过多的开销。由于涉及如此多的操作,性能损失是不可避免的,不幸的是,我们没有太多机会来优化它。
因此,如果您关心性能,建议您远离 AppDomain,就像我们通常做的那样。但有时,您只需要使用另一个 AppDomain,例如,当您正在实现插件架构并且需要在运行时加载和卸载程序集而不停止应用程序时。
在这些情况下,一个快速的跨 AppDomain 封送器会非常有帮助。这就是 JointCode.Shuttle 的用武之地。
如何使用
在 JointCode.Shuttle 发布包中,它包含两个文件:JointCode.Shuttle.dll 和 JointCode.Shuttle.Library.dll,其中 JointCode.Shuttle.dll 是用托管语言编写的库,而 JointCode.Shuttle.Library.dll 是一个用非托管语言编写的组件,前者依赖于它。
准备
要使用 JointCode.Shuttle,我们首先需要在我们的项目中引用 JointCode.Shuttle.dll,并将 JointCode.Shuttle.Library.dll 复制到 JointCode.Shuttle.dll 编译到的文件夹中(例如,在项目编译后,如果 JointCode.Shuttle.dll 被复制到 c:/projects/sampleproject 文件夹,您需要手动将 JointCode.Shuttle.Library.dll 复制到此文件夹)。
使用代码
由于 JointCode.Shuttle 是面向接口的,所以我们首先需要创建一个服务接口(也称为服务契约),并将 ServiceInterface
属性应用于它。
[ServiceInterface]
public interface IServiceFunctionTest
{
// properties and methods ...
}
然后我们创建一个实现该契约的服务类,并将 ServiceClass
属性应用于它。
[ServiceClass(typeof(IServiceFunctionTest), Lifetime = LifetimeEnum.Transient)]
public class ServiceFunctionTest : IServiceFunctionTest
{
// properties and methods
}
因为我们想要进行跨 AppDomain
调用,我们需要编写一个类来启动/停止远程服务,并让它继承自 MarshalByRefObject
。
/// <summary>
/// A remote service end
/// </summary>
/// <seealso cref="System.MarshalByRefObject" />
public abstract class RemoteServiceEnd : MarshalByRefObject
{
protected ShuttleDomain _shuttleDomain;
/// <summary>
/// Creates a ShuttleDomain instance in current AppDomain, so that we can communicate with other AppDomains.
/// </summary>
public void CreateShuttleDomain()
{
var key = this.GetType().Name;
_shuttleDomain = ShuttleDomainHelper.Create(key, key);
}
/// <summary>
/// Unregister all services registered to the global registry from the current AppDomain, and disposes the ShuttleDomain.
/// </summary>
public void DisposeShuttleDomain()
{
if (_shuttleDomain != null)
_shuttleDomain.Dispose();
}
/// <summary>
/// Registers services to a global registy from current AppDomain.
/// </summary>
public abstract void RegisterServices(); // provide services
/// <summary>
/// Consumes services registered by other providers in other AppDomains.
/// </summary>
public abstract void ConsumeServices(); // consume services
}
public class RemoteServiceEnd1 : RemoteServiceEnd
{
public override void RegisterServices()
{
var guid = Guid.NewGuid();
_shuttleDomain.RegisterServiceGroup(ref guid,
new ServiceTypePair(typeof(ICommonService), typeof(CommonService)),
new ServiceTypePair(typeof(IServiceFunctionTest), typeof(ServiceFunctionTest)));
}
public override void ConsumeServices()
{
IFakeService fakeService;
if (_shuttleDomain.TryGetService(out fakeService))
{
Console.WriteLine("AppDomain [{0}], before calling the remote service: ", AppDomain.CurrentDomain.FriendlyName);
var result = fakeService.PrintAndReturn("JointCode.Shuttle");
Console.WriteLine("AppDomain [{0}], after calling the remote service with result [{1}] ", AppDomain.CurrentDomain.FriendlyName, result);
Console.WriteLine();
}
}
}
现在,我们准备好使用 JointCode.Shuttle 了,像这样
// To be able to make inter-AppDomain communication with JointCode.Shuttle, firstly we must
// initialize the ShuttleDomain.
// It doesn't matter whether the initialization operation is done in default AppDomain or
// any other AppDomains, but it must be done before any ShuttleDomain instance is created.
ShuttleDomain.Initialize();
// Create a remote AppDomain
_remoteDomain = AppDomain.CreateDomain(Guid.NewGuid().ToString(), null, null);
// Create a service end, which will be used as a proxy to operate the remote AppDomain
_serviceEnd1 = (RemoteServiceEnd)_remoteDomain.CreateInstanceAndUnwrap
(_serviceEnd1AsmName, ServiceEnd1Type);
// Create a ShuttleDomain instance in remote AppDomain
_serviceEnd1.CreateShuttleDomain();
var key = Guid.NewGuid().ToString();
// Create a ShuttleDomain instance in current AppDomain
_shuttleDomain = ShuttleDomainHelper.Create(key, key);
// Register services through the above ShuttleDomain instance in remote AppDomain
_serviceEnd1.RegisterServices();
// Get a remote service instance in current AppDomain, which is created in remote AppDomain
_shuttleDomain.TryGetService(out _shuttleFunctionTest);
// Call the remote service method from current AppDomain
_shuttleFunctionTest.CallSimpleMethod();
测试
为了比较 JointCode.Shuttle 和 MarshalByrefObject 的跨 AppDomain 调用性能,并尝试了解可能影响性能的各种因素,我构建了一个测试。这个测试向我们展示了两种基准
- 重复创建远程服务对象并调用对象方法:这是为了比较两种调用作为一个整体的性能,因为对象创建和方法调用都会消耗时间。
- 创建一个远程服务对象并将其缓存到本地字段中,然后重复调用该对象的方法:这是为了测量具有不同种类和数量参数的方法调用的性能,因为参数需要在 AppDomain 之间进行封送,这也会消耗时间。请注意,这仅仅是为了测试的方便,永远不要将远程服务对象缓存到实践中的字段中。由于远程服务对象的生命周期由远程端控制,因此远程对象可能已经在远程端过期并被垃圾回收,而本地调用者并不知道。在这种情况下,如果我们继续调用它的方法,将会抛出一个异常。
您可以在源代码下载中查看基准应用程序的完整代码。这是部分结果的屏幕截图
结论
JointCode.Shuttle 可以显着提高跨 AppDomain 通信的性能,并且它带来了 许多其他技术无法提供的优势。
未来
JointCode.Shuttle 仍然是一个年轻的项目,它缺少一些功能,例如服务注册/取消注册通知、跨 AppDomain 事件等。作者将继续改进现有功能,并在未来引入更多新功能,但目前存在一些限制,包括
- 仅支持 32 位应用程序(x86 目标平台)
- 仅支持 Windows(目前仅支持 .net framework,不支持 mono)
- 服务接口中不支持事件
- 不支持跨 AppDomain 事件
- 未经过彻底测试