如果使用具有大对象的枚举,Parallel.ForEach可能会导致"内存不足"异常

Sco*_*ain 63 c# out-of-memory large-data task-parallel-library

我正在尝试将数据库中存储图像的数据库迁移到指向硬盘驱动器上的文件的数据库中的记录.我试图使用此方法来查询数据Parallel.ForEach以加快进程.

但是,我注意到我得到了一个OutOfMemory例外.我知道Parallel.ForEach会查询一批枚举,以减少开销的成本,如果有一个用于间隔查询(如果您一次执行一堆查询而不是间隔它们,您的源将更有可能将下一条记录缓存在内存中)出).问题是由于我返回的记录之一是1-4Mb字节数组,缓存导致整个地址空间用完(程序必须在x86模式下运行,因为目标平台将是32位机)

是否有任何方法可以禁用缓存或使TPL更小?


这是一个显示问题的示例程序.这必须在x86模式下编译,以显示问题,如果它在你的机器上花费很长时间或者没有发生,从而增加了阵列的大小(我发现1 << 20我的机器上大约需要30秒,4 << 20几乎是瞬间的)

class Program
{

    static void Main(string[] args)
    {
        Parallel.ForEach(CreateData(), (data) =>
            {
                data[0] = 1;
            });
    }

    static IEnumerable<byte[]> CreateData()
    {
        while (true)
        {
            yield return new byte[1 << 20]; //1Mb array
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Ric*_*key 95

默认选项Parallel.ForEach 仅在任务受CPU约束且线性扩展时才能正常工作.当任务受CPU约束时,一切都很完美.如果您有四核并且没有其他进程在运行,则Parallel.ForEach使用所有四个处理器.如果您的计算机上有四核和其他一些进程正在使用一个完整的CPU,则Parallel.ForEach使用大约三个处理器.

但是如果任务不受CPU约束,那么Parallel.ForEach继续启动任务,努力保持所有CPU忙碌.然而,无论并行运行多少任务,总会有更多未使用的CPU马力,因此它不断创建任务.

如何判断您的任务是否受CPU限制?希望只是通过检查它.如果你考虑素数,很明显.但其他情况并不那么明显.判断您的任务是否受CPU限制的经验方法是限制最大并行度ParallelOptions.MaximumDegreeOfParallelism并观察程序的行为方式.如果您的任务是CPU限制的,那么您应该在四核系统上看到这样的模式:

  • ParallelOptions.MaximumDegreeOfParallelism = 1:使用一个完整的CPU或25%的CPU利用率
  • ParallelOptions.MaximumDegreeOfParallelism = 2:使用两个CPU或50%的CPU利用率
  • ParallelOptions.MaximumDegreeOfParallelism = 4:使用所有CPU或100%CPU利用率

如果它的行为如此,那么您可以使用默认Parallel.ForEach选项并获得良好的结果.线性CPU利用率意味着良好的任务调度

但是,如果我在我的Intel i7上运行您的示例应用程序,无论我设置的最大并行度如何,我都可以获得大约20%的CPU利用率.为什么是这样?正在分配垃圾收集器阻塞线程的内存.应用程序受资源限制,资源是内存.

同样,对数据库服务器执行长时间运行查询的I/O绑定任务也永远无法有效地利用本地计算机上可用的所有CPU资源.在这种情况下,任务调度程序无法"知道何时停止"启动新任务.

如果您的任务不受CPU限制或CPU利用率不能以最大并行度线性扩展,那么您应该建议Parallel.ForEach不要一次启动太多任务.最简单的方法是指定一个允许重叠I/O绑定任务的并行性的数字,但不要太多,以至于压倒本地计算机对资源的需求或使任何远程服务器过载.试用和错误是为了获得最佳结果:

static void Main(string[] args)
{
    Parallel.ForEach(CreateData(),
        new ParallelOptions { MaxDegreeOfParallelism = 4 },
        (data) =>
            {
                data[0] = 1;
            });
}
Run Code Online (Sandbox Code Playgroud)

  • 我认为这与我的问题有关,他在头上打了一针.我可能会使用[Enviorment.ProcessorCount](http://msdn.microsoft.com/en-us/library/system.environment.processorcount.aspx)并将其设置为最大并行度的极限 (22认同)

Dre*_*rsh 41

所以,虽然里克提出的建议绝对是一个重点,但我认为缺少的另一件事是讨论分区.

Parallel::ForEach将使用默认Partitioner<T>实现,对于IEnumerable<T>没有已知长度的实现,将使用块分区策略.这意味着Parallel::ForEach将用于处理数据集的每个工作线程将从中读取一些元素,IEnumerable<T>然后仅由该线程处理(暂时忽略工作窃取).它这样做是为了节省不断返回源代码并分配一些新工作并为另一个工作线程安排它的费用.所以,通常情况下,这是一件好事.但是,在你的特定场景中,想象你是一个四核心,并且你已经MaxDegreeOfParallelism为你的工作设置了4个线程,现在每个线程从你的工作中提取了100个元素IEnumerable<T>.那么,那个100-400兆就在那个特定的工人线上,对吗?

那你怎么解决这个问题呢?很简单,你写一个自定义的Partitioner<T>实现.现在,在你的情况下,分块仍然很有用,所以你可能不想使用单个元素分区策略,因为那样你就会引入所需的所有任务协调的开销.相反,我会编写一个可配置的版本,您可以通过appsetting调整,直到找到工作负载的最佳平衡.好消息是,虽然编写这样的实现是非常直接的,但实际上你甚至不必自己编写,因为PFX团队已经完成并将其放入并行编程示例项目中.

  • 从.NET 4.5开始,.NET框架通过[`Partitioner.Create(CreateData(),EnumerablePartitionerOptions.NoBuffering)`]提供单个元素分区程序(https://msdn.microsoft.com/en-us/library/ hh136328(v = vs.110)的.aspx) (2认同)

evo*_*obe 14

这个问题与分区程序有关,而与并行程度无关.解决方案是实现自定义数据分区程序.

如果数据集很大,似乎TPL的单声道实现保证耗尽内存.这最近发生在我身上(基本上我正在运行上面的循环,并发现内存线性增加,直到它给我一个OOM异常).

在跟踪问题之后,我发现默认情况下mono将使用EnumerablePartitioner类来划分枚举器.这个类的行为在于,每当它向任务提供数据时,它就会以不断增加的(并且不可更改的)因子2"数据块化"数据.因此,当任务第一次请求数据时,它会获得一大块的大小1,下一次大小2*1 = 2,下一次2*2 = 4,然后2*4 = 8等等.结果是交给任务的数据量,因此存储在内存同时,随着任务的长度而增加,如果正在处理大量数据,则不可避免地会发生内存不足异常.

据推测,这种行为的最初原因是它希望避免每个线程多次返回以获取数据,但它似乎是基于所有正在处理的数据都适合内存的假设(不是从读取时的情况)大文件).

如前所述,使用自定义分区程序可以避免此问题.一个简单地将数据一次返回到每个任务一个项目的一个通用示例如下:

https://gist.github.com/evolvedmicrobe/7997971

首先简单地实例化该类,然后将其交给Parallel.For而不是枚举本身