暂停线程直到有足够的内存可用。

3

环境:.net 4.0

我有一个任务,需要使用XSLT样式表转换XML文件,以下是我的代码:

public string TransformFileIntoTempFile(string xsltPath, 
    string xmlPath)
{
    var transform = new MvpXslTransform();
    transform.Load(xsltPath, new XsltSettings(true, false), 
        new XmlUrlResolver());

    string tempPath = Path.GetTempFileName();

    using (var writer = new StreamWriter(tempPath))
    {
        using (XmlReader reader = XmlReader.Create(xmlPath))
        {
            transform.Transform(new XmlInput(reader), null, 
                new XmlOutput(writer));
        }       
    }

    return tempPath;
}

我有X个线程可以并行启动此任务。有时我的输入文件大约300MB,有时只有几MB。
我的问题:当程序尝试同时转换一些大型XML文件时,我会收到OutOfMemoryException(内存溢出异常)。
如何避免这些OutOfMemoryEception?我的想法是在执行任务之前停止一个线程,直到有足够的可用内存,但我不知道如何做到这一点。或者有其他解决方案(比如将我的任务放在不同的应用程序中)。
谢谢

你使用的是哪个版本的.NET框架? - sll
1
32位程序很少会缩小其虚拟内存大小或整理其地址空间。建议使用64位操作系统。 - Hans Passant
@hans:是的,但你知道从32位到64位需要做很多事情,而且我们实际上没有升级我们的硬件/操作系统的计划。 - remi bourgarel
4个回答

2
我不建议阻塞线程。最坏的情况是,你会饿死可能会释放所需内存的任务,导致死锁或者整体性能非常糟糕。
相反,我建议您使用具有优先级的工作队列。从队列中公平地安排任务到线程池中。确保没有线程在等待操作上被阻塞,而是将任务重新发布到队列中(降低优先级)。
因此,当收到 OutOfMemory 异常时,您需要将相同的作业/任务发布到队列中并终止当前任务,以释放线程以供其他任务使用。
一个简单的方法是使用 LIFO,它确保发布到队列中的任务的“优先级”低于该队列中已有的任何其他作业。

你有这种队列的任何示例吗? - remi bourgarel
我建议坚持使用TPL,例如请参考https://dev59.com/3lnUa4cB1Zd3GeqPannX - sehe

1

自从 .NET Framework 4,我们就有了 API 来处理好老旧的内存映射文件功能,这个功能在 Win32API 中已经存在多年了,现在你可以从 .NET 托管代码中使用它。

对于您的任务来说,“持久化内存映射文件”选项更适合,MSDN

持久化文件是与磁盘上源文件相关联的内存映射文件。当最后一个进程完成对文件的操作后,数据将保存到磁盘上的源文件中。这些内存映射文件适用于处理极大的源文件。

MemoryMappedFile.CreateFromFile() 方法描述页面上,您可以找到一个很好的示例,描述如何为极大的文件创建内存映射视图。

编辑:根据评论中的重要说明进行更新。

刚刚发现了一个方法MemoryMappedFile.CreateViewStream(),它创建了一个类型为MemoryMappedViewStream的流,该流继承自System.IO.Stream。我相信您可以从这个流中创建XmlReader的实例,然后使用此读取器/流来实例化XslTransform的自定义实现。

编辑2:remi bourgarel(OP)已经测试了这种方法,看起来这个特定的XslTransform实现(我想知道是否有任何实现)不能按照预期的方式与MM-View流一起工作。


1
它是否与XSLT转换兼容?.net在转换之前会加载整个xml文件,对吗? - remi bourgarel
1
有没有证据表明XML类不仅会使用流将数据复制到内存中? - Tim Lloyd
@chibacity:我真的不确定MM-Stream是否会比标准流给我们带来任何好处。这是一个指针,OP可以自己检查并向我们提供有关此新功能的更多信息。 - sll
2
@sll,我刚试了你的解决方案,但Xslt转换仍然会加载整个文档(我反编译了Xslt转换类并发现它使用XPathDocument来加载整个XML)。 - remi bourgarel
2
@sll,这是所有 .net 的 XSLT 实现(无论是 MVP 还是非 MVP)的情况,XSLT 需要加载完整的 XML 文档。而且我不能重写 Xslt 的实现,对我来说似乎不是几个小时的工作。 - remi bourgarel
显示剩余9条评论

0
你可以考虑使用队列来限制并发转换的数量,基于某种人工内存边界,例如文件大小。可以使用以下类似的内容:
这种限流策略可以与最大并发文件数相结合,以确保您的磁盘不会受到过多的影响。
注意:我没有包括必要的try\catch\finally来确保异常传播到调用线程并且等待处理总是被释放。我可以在这里进一步详细说明。
public static class QueuedXmlTransform
{
    private const int MaxBatchSizeMB = 300;
    private const double MB = (1024 * 1024);
    private static readonly object SyncObj = new object();
    private static readonly TaskQueue Tasks = new TaskQueue();
    private static readonly Action Join = () => { };
    private static double _CurrentBatchSizeMb;

    public static string Transform(string xsltPath, string xmlPath)
    {
        string tempPath = Path.GetTempFileName();

        using (AutoResetEvent transformedEvent = new AutoResetEvent(false))
        {
            Action transformTask = () =>
            {
                MvpXslTransform transform = new MvpXslTransform();

                transform.Load(xsltPath, new XsltSettings(true, false),
                    new XmlUrlResolver());

                using (StreamWriter writer = new StreamWriter(tempPath))
                using (XmlReader reader = XmlReader.Create(xmlPath))
                {
                    transform.Transform(new XmlInput(reader), null,
                        new XmlOutput(writer));
                }

                transformedEvent.Set();
            };

            double fileSizeMb = new FileInfo(xmlPath).Length / MB;

            lock (SyncObj)
            {
                if ((_CurrentBatchSizeMb += fileSizeMb) > MaxBatchSizeMB)
                {
                    _CurrentBatchSizeMb = fileSizeMb;

                    Tasks.Queue(isParallel: false, task: Join);
                }

                Tasks.Queue(isParallel: true, task: transformTask);
            }

            transformedEvent.WaitOne();
        }

        return tempPath;
    }

    private class TaskQueue
    {
        private readonly object _syncObj = new object();
        private readonly Queue<QTask> _tasks = new Queue<QTask>();
        private int _runningTaskCount;

        public void Queue(bool isParallel, Action task)
        {
            lock (_syncObj)
            {
                _tasks.Enqueue(new QTask { IsParallel = isParallel, Task = task });
            }

            ProcessTaskQueue();
        }

        private void ProcessTaskQueue()
        {
            lock (_syncObj)
            {
                if (_runningTaskCount != 0) return;

                while (_tasks.Count > 0 && _tasks.Peek().IsParallel)
                {
                    QTask parallelTask = _tasks.Dequeue();

                    QueueUserWorkItem(parallelTask);
                }

                if (_tasks.Count > 0 && _runningTaskCount == 0)
                {
                    QTask serialTask = _tasks.Dequeue();

                    QueueUserWorkItem(serialTask);
                }
            }
        }

        private void QueueUserWorkItem(QTask qTask)
        {
            Action completionTask = () =>
            {
                qTask.Task();

                OnTaskCompleted();
            };

            _runningTaskCount++;

            ThreadPool.QueueUserWorkItem(_ => completionTask());
        }

        private void OnTaskCompleted()
        {
            lock (_syncObj)
            {
                if (--_runningTaskCount == 0)
                {
                    ProcessTaskQueue();
                }
            }
        }

        private class QTask
        {
            public Action Task { get; set; }
            public bool IsParallel { get; set; }
        }
    }
}

更新

修复了在滚动到下一个批处理窗口时维护批处理大小的错误:

_CurrentBatchSizeMb = fileSizeMb;

0
主要问题是您正在加载整个Xml文件。如果您只是在读取时进行转换,则通常不应出现内存不足的问题。 话虽如此,我找到了一篇微软支持文章,介绍了如何完成此操作: http://support.microsoft.com/kb/300934 免责声明:我没有测试过这个方法,如果您使用它并且有效,请告诉我们。

你链接中的代码只是建议使用XPathDocument,我不认为它真正支持流式处理。 - remi bourgarel
“transform as you read” 可能不适用于 XsltTransformation,因为 XPath 查询可以跨越整个 XML 树。 - remi bourgarel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接