使用 AppDomain 存储大型数据集合
如何使用 AppDomain 存储大型数据集合
引言
您使用的是 32 位操作系统吗?您编写的是 32 位应用程序吗?您可能对为您的应用程序获取更多内存感兴趣;无需硬件!秘密是什么?AppDomains!
背景
本文我将只讨论虚拟内存。真实内存和虚拟内存之间的关系超出了本文的范围,并且已在许多其他文章中广泛介绍。
在您的应用程序运行之前,公共语言运行时 (CLR) 会为您创建一个默认域。CLR 会自动加载 _mscorlib.dll_、您的程序以及可能其他程序集。因此,默认域有 4GB 的可寻址内存,但应用程序可用的内存量较小;通常是 2 到 3 GB 的虚拟存储。
您的程序可以创建额外的域。我将应用程序创建的域称为辅助域。在 32 位系统上,一个辅助域将有 2GB 的可寻址内存,在加载 _mscorlib.dll_ 和可能其他程序集之后,可用内存介于 1 到 2 GB 之间。您可以有多个辅助域,一个辅助域也可以创建其他域。
我们可以用额外的内存做什么?
大多数文章和书籍都谈论使用辅助域来创建一个隔离环境,其中执行不受信任的应用程序。我想使用额外的内存来存储数据。辅助 AppDomain 是用于大型集合和数据缓存的绝佳选择。
我们如何使用 AppDomain,以及它们的限制是什么?
.NET CLR 创建默认域,但您必须创建任何辅助域。在辅助域的内存中存储和访问数据的代码也必须编写。我将向您展示如何创建和处置辅助域,以及如何向辅助域添加可执行代码。您编写的代码必须考虑以下限制。
- 每个 AppDomain 都有自己的地址空间。一个域中的内存与其他域隔离。数据必须在域之间显式传递。
- 在域之间传递的数据必须是可序列化的。幸运的是,许多标准集合已经被标记为可序列化。您可以使用
[Serializable]
属性标记您自己的类和结构。 - 迭代器和枚举器不可序列化;
foreach
和collection.ForEach
语句不能跨域边界工作。替代方法是数组下标或处理集合的子集。“AddRange(elementList);
”和“List<string> elements = GetRange(from, to);
”是保存或获取集合子集的方法示例。 - 构造函数不能有参数。您需要使用属性和函数将初始化参数传递给对象。其他函数可能需要修改以确保对象构造已完全初始化。
费用
- 传统的权衡之一是使用更多内存以换取更快的执行速度。在这种情况下,内存与时间之间的权衡并非如此;序列化和反序列化数据需要时间。应用程序将比数据存储在与处理数据的代码相同的域中运行时慢。但是,有可能将数据处理转移到辅助域,这将消除大部分开销。
- 异常处理必须仔细规划。在辅助域中抛出的异常必须在辅助域中处理。如果未处理,默认域将通过
UnhandledExceptionEventHandler
事件接收异常通知,并且应用程序将终止。
使用跨域对象
示例程序使用泛型字符串集合来演示跨域对象。示例程序在主域的本地内存或辅助域中创建和处理字符串集合。泛型集合名为 DomainList<T>
。DomainList<T>
是泛型 List<T>
类的包装器。示例程序生成一组随机数,这些数字转换为固定长度的字符串。例如,数字 4567 转换为右对齐、用空格填充的字面量“ 4567”。字面量字符串存储在 DomainList<T>
类的实例中。通常,这将编码为
1. DomainList<string> storedText = new DomainList<string>(); 2. storedText.Add(rightJustifiedNumber); 3. storedText.Dispose();
语句 1 创建一个适用于字符串数据的 DomainList 对象实例。
语句 2 将一个字符串添加到对象。该字符串将存储在当前域的内存中。
语句 3 处置 storedText 对象并释放与该对象关联的内存。
要将数据存储在不同域的内存中,您需要创建一个 AppDomain 并在新创建的域中创建 DomainList<T>
对象的实例。以下代码是完成这些操作的最简单方法。
1. AppDomain storageDomain = AppDomain.CreateDomain("SecondaryDomain"); 2. DomainList<string> storedText = (DomainList<string>)storageDomain.CreateInstanceAndUnwrap( typeof(DomainList<string>).Assembly.FullName, typeof(DomainList<string>).FullName); 3. storedText.Add(rightJustifiedNumber); 4. storedText.Dispose(); 5. AppDomain.Unload(storageDomain);
语句 1 创建一个新域并为新域指定名称。域的名称应适合显示。
语句 2 在当前域和辅助域中创建 DomainList<T>
对象的实例。它还建立了跨域通信环境,允许您在两个域之间传递数据。可以在辅助域中创建多个对象。
语句 3 将一个字符串添加到对象。该字符串将存储在辅助域的内存中。
语句 4 处置 storedText
对象并释放与该对象关联的内存。AppDomain 必须单独处置。
语句 5 处置辅助域。您可以在辅助域中拥有多个跨域对象。在该域中所有跨域对象都被处置后,处置该域。
DomainList<T> 类
DomainList<T> 对象是泛型 List<T>
集合的包装器。List<T>
类提供了对集合中数据进行操作的函数。DomainList<T>
提供了跨域通信所需的必要信息和函数定义,并增强了一些功能。需要包装器有几个原因。
- 继承是不可行的。
DomainList<T>
已经继承自MarshalByRefObject
或等效类。C# 不支持多重继承,所以我通过在DomainList<T>
对象中创建List<T>
对象的实例来包装List<T>
。然后我编写了所需函数的增强版本。最终用户应用程序将调用DomainList<T>
的增强函数,然后该函数将调用List<T>
的等效函数来执行工作。 - 数据必须是可序列化的。枚举和迭代器不可序列化。您可能需要提供替代的迭代方法,例如下标或按范围检索数据。我创建了
Add(mySublist)
、Remove(startIndex, endIndex)
和Retrieve(startIndex, endIndex)
函数来加速大容量数据交换。Sort()
函数在辅助域中运行,从而消除了大量的跨域数据序列化/反序列化。 - 您不能使用 throw/catch 逻辑跨域边界来处理异常。您必须使用其他技术来传达错误条件。增强的
DomainList<T>
函数在函数内部使用 try/catch 逻辑来捕获List<T>
抛出的异常。异常通过DomainList<T>
的LastException
属性返回给最终用户。DomainList<T>
通常会创建一个新的Exception
并使用Source
属性来识别在抛出异常时正在执行的函数。新异常的InnerException
属性返回原始错误。在可行的情况下,也会使用不正确的返回值。例如,如果无法确定List<T>
中的元素数量,则 x.Count 将返回 -1。缺点是显而易见的;调用者必须检查LastException
属性,否则错误将消失。
LastException
属性的概念对于单线程类来说效果很好,但对于多线程类则不然。您可能会错过一个线程中的错误,或者在未进行错误调用的线程中收到错误通知。下标表示法的错误反馈仍然存在问题。
编码 DomainList<T>
类与编码普通类非常相似。类声明是
[Serializable] public class DomainList<T> : MarshalByRefObject, IDisposable
可序列化属性将类标记为可序列化,并且是必需的。
MarshalByRefObject
是通过代理跨应用程序域边界通信的对象的基类。同一域内的对象直接通信;无需代理。
强烈推荐 IDisposable
,以便尽快释放跨域资源。
CrossAppDomainObject
在跨域环境中,处置继承自 MarshalByRefObject
的对象将导致内存泄漏。必须释放域以释放所有资源。您应该继承自 CrossAppDomainObject
类,而不是 MarshalByRefObject
。Nathan B. Evans 编写了 CrossAppDomainObject
类并将其发布在 http://nbevans.wordpress.com/2011/04/17/memory-leaks-with-an-infinite-lifetime-instance-of-marshalbyrefobject/
他的解释比我的好得多,但我会指出编码上的差异。将 public class DomainList<T> : MarshalByRefObject, IDisposable
更改为 public class DomainList<T> : CrossAppDomainObject, IDisposable
。当继承自 CrossAppDomainObject
时,IDisposable
是可选的,因为 CrossAppDomainObject
包含处置代码。如果您的类中有需要处置的对象,您需要重写 CrossAppDomainObject
中的 Dispose
方法。有关完整的编码示例,请参阅示例项目中的 DomainListCD<T>
对象。
示例程序
我提供了三个示例项目。我将所有示例编译为 x86 处理器的平台目标。无论操作系统模式如何,这些示例都将使用 32 位寻址。这对于第三个示例项目尤其重要。所有项目都将在 64 位模式下工作,但内存限制不容易演示。
DomainMemoryDemo 和 DomainMemoryDemoCrossAppDomain 是相同的程序,只是 DomainList<T>
继承自 MarshalByRefObject
,而 DomainListCD<T>
继承自 CrossAppDomainObject
。您可以在默认域或辅助域中运行对象测试。“测试对象”复选框将对生成的数据执行多项操作,以演示一些编码技术。“预分配内存”复选框将在填充集合之前为集合提供容量。我添加此复选框只是为了查看它对加载时间产生多大影响。
生成的报告将显示各种操作的经过时间和 CPU 时间。例如,排序的 CPU 时间很长,并且将显示在数据存储的域中。您还会看到数据加载时间的巨大差异;默认域的经过时间为 3.22 秒,而辅助域的经过时间为 16.27 秒。这是使用辅助域的开销。
第三个示例项目是 DomainMemoryLeakTest。此测试将创建和处置 DomainMemory 对象,直到内存耗尽或您停止测试。“正常处置”与“创建一次域”在 32 位系统上应在大约一分钟内失败。所有其他组合将继续执行,直到您按下“停止”按钮。
关注点
这些对象在 64 位环境中也运行良好。无论环境如何,对象大小都不能超过 2GB,除非设置了 gcAllowVeryLargeObjects
(仅限 .NET 4.5+ 和 64 位操作系统)。请参阅
- http://msdn.microsoft.com/en-us/library/hh285054%28v=vs.110%29.aspx
- http://msdn.microsoft.com/en-us/library/vstudio/ms184658%28v=vs.110%29.aspx
将应用程序配置文件添加到您的 C# 项目
- 在菜单栏上,选择“项目”,“添加新项”。
- “添加新项”对话框出现。
- 展开“已安装”,展开“Visual C# 项”,然后选择“应用程序配置文件”模板。
- 在“名称”文本框中,输入一个名称,然后选择“添加”按钮。一个名为 app.config 的文件将添加到您的项目中。
- 将包含
gcAllowVeryLargeObjects
的行添加到您的配置文件中。
一个示例配置文件是
<configuration> <runtime> <gcAllowVeryLargeObjects enabled="true" /> </runtime> </configuration>
历史
这是版本 1.0.0.0
致谢
所有示例均使用 Microsoft Visual Studio Community 2013 和 Microsoft Visual Studio 2010 Express,.Net Framework 4 Client Profile 开发和测试。我还在 Visual Studio 2010 Express 下进行了测试。感谢 Nathan B. Evans 在 CrossAppDomainObject 方面做出的出色工作。请参阅