65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 AppDomain 存储大型数据集合

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (4投票s)

2015年2月12日

CPOL

10分钟阅读

viewsIcon

22839

downloadIcon

277

如何使用 AppDomain 存储大型数据集合

引言

您使用的是 32 位操作系统吗?您编写的是 32 位应用程序吗?您可能对为您的应用程序获取更多内存感兴趣;无需硬件!秘密是什么?AppDomains!

背景

本文我将只讨论虚拟内存。真实内存和虚拟内存之间的关系超出了本文的范围,并且已在许多其他文章中广泛介绍。

在您的应用程序运行之前,公共语言运行时 (CLR) 会为您创建一个默认域。CLR 会自动加载 _mscorlib.dll_、您的程序以及可能其他程序集。因此,默认域有 4GB 的可寻址内存,但应用程序可用的内存量较小;通常是 2 到 3 GB 的虚拟存储。

您的程序可以创建额外的域。我将应用程序创建的域称为辅助域。在 32 位系统上,一个辅助域将有 2GB 的可寻址内存,在加载 _mscorlib.dll_ 和可能其他程序集之后,可用内存介于 1 到 2 GB 之间。您可以有多个辅助域,一个辅助域也可以创建其他域。

我们可以用额外的内存做什么?

大多数文章和书籍都谈论使用辅助域来创建一个隔离环境,其中执行不受信任的应用程序。我想使用额外的内存来存储数据。辅助 AppDomain 是用于大型集合和数据缓存的绝佳选择。

我们如何使用 AppDomain,以及它们的限制是什么?

.NET CLR 创建默认域,但您必须创建任何辅助域。在辅助域的内存中存储和访问数据的代码也必须编写。我将向您展示如何创建和处置辅助域,以及如何向辅助域添加可执行代码。您编写的代码必须考虑以下限制。

  1. 每个 AppDomain 都有自己的地址空间。一个域中的内存与其他域隔离。数据必须在域之间显式传递。
  2. 在域之间传递的数据必须是可序列化的。幸运的是,许多标准集合已经被标记为可序列化。您可以使用 [Serializable] 属性标记您自己的类和结构。
  3. 迭代器和枚举器不可序列化;foreachcollection.ForEach 语句不能跨域边界工作。替代方法是数组下标或处理集合的子集。“AddRange(elementList);”和“List<string> elements = GetRange(from, to);”是保存或获取集合子集的方法示例。
  4. 构造函数不能有参数。您需要使用属性和函数将初始化参数传递给对象。其他函数可能需要修改以确保对象构造已完全初始化。

费用

  1. 传统的权衡之一是使用更多内存以换取更快的执行速度。在这种情况下,内存与时间之间的权衡并非如此;序列化和反序列化数据需要时间。应用程序将比数据存储在与处理数据的代码相同的域中运行时慢。但是,有可能将数据处理转移到辅助域,这将消除大部分开销。
  2. 异常处理必须仔细规划。在辅助域中抛出的异常必须在辅助域中处理。如果未处理,默认域将通过 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> 提供了跨域通信所需的必要信息和函数定义,并增强了一些功能。需要包装器有几个原因。

  1. 继承是不可行的。DomainList<T> 已经继承自 MarshalByRefObject 或等效类。C# 不支持多重继承,所以我通过在 DomainList<T> 对象中创建 List<T> 对象的实例来包装 List<T>。然后我编写了所需函数的增强版本。最终用户应用程序将调用 DomainList<T> 的增强函数,然后该函数将调用 List<T> 的等效函数来执行工作。
  2. 数据必须是可序列化的。枚举和迭代器不可序列化。您可能需要提供替代的迭代方法,例如下标或按范围检索数据。我创建了 Add(mySublist)Remove(startIndex, endIndex)Retrieve(startIndex, endIndex) 函数来加速大容量数据交换。Sort() 函数在辅助域中运行,从而消除了大量的跨域数据序列化/反序列化。
  3. 您不能使用 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 位操作系统)。请参阅

将应用程序配置文件添加到您的 C# 项目

  1. 在菜单栏上,选择“项目”,“添加新项”。
  2. “添加新项”对话框出现。
  3. 展开“已安装”,展开“Visual C# 项”,然后选择“应用程序配置文件”模板。
  4. 在“名称”文本框中,输入一个名称,然后选择“添加”按钮。一个名为 app.config 的文件将添加到您的项目中。
  5. 将包含 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 方面做出的出色工作。请参阅

© . All rights reserved.