异步加载 XDocument

14

我想将大型XML文档加载到XDocument对象中。 使用XDocument.Load(path, loadOptions)的简单同步方法非常好用,但在GUI上下文中加载大文件(尤其是来自网络存储)时会阻塞相当长的时间。

我编写了这个异步版本,旨在提高文档加载的响应能力,特别是在从网络加载文件时。

    public static async Task<XDocument> LoadAsync(String path, LoadOptions loadOptions = LoadOptions.PreserveWhitespace)
    {
        String xml;

        using (var stream = File.OpenText(path))
        {
            xml = await stream.ReadToEndAsync();
        }

        return XDocument.Parse(xml, loadOptions);
    }

然而,在从本地磁盘加载的 200 MB XML 原始文件上,同步版本可以在几秒钟内完成。异步版本(在32位环境下运行)则抛出 OutOfMemoryException 异常:

   at System.Text.StringBuilder.ToString()
   at System.IO.StreamReader.<ReadToEndAsyncInternal>d__62.MoveNext()

我想这是因为在解析XDocument时,使用了临时字符串变量来保存原始XML数据。可以想象,在同步场景中,XDocument.Load()能够通过源文件进行流式传输,并且不需要创建一个巨大的字符串来保存整个文件。

有没有办法兼顾两者优点?使用完全异步I/O加载XDocument,而无需创建大型临时字符串?


也许你应该使用 XDocument.Load(stream) - DavidG
@DavidG 这正是 OP 所说的 他们已经做了。但他们需要异步执行操作,而不是同步执行。 - Servy
4
等待 这个 或尝试自己做。 - Paulo Morgado
我现在正在做的是使用await Task.Run()在后台任务中调用XDocument.Load(String path, LoadOptions options)。这不是真正的异步IO,因为它使用线程池线程来运行加载过程,可能会在底层等待IO,而不是由IO事件驱动。但这可能已经足够好了。 - Hydrargyrum
根据堆栈跟踪,您可能可以使用MemoryStream将整个内容加载到内存中。 然后将MemoryStream.Position设置为0,并使用XDocument(同步)加载它。 这样,您就避免了需要创建一个200MB的字符串(这实际上可能会变成400MB,因为.NET UTF-16编码的文件很可能大部分是ASCII,并且使用UTF-8编码到200MB)。 但是,接受的答案允许您完全避免构建单独的缓冲区,在这种环境下,即使有阻塞,它仍然是最佳选择。 - binki
显示剩余2条评论
3个回答


2
晚了点回答,但我也需要在一个“传统”的.NET Framework版本上进行异步读取,因此我找到了一种真正异步读取内容而不必将XML数据缓冲到内存中的方法。
由于XDocument.CreateWriter()提供的写入器不支持异步写入,因此XmlWriter.WriteNodeAsync()失败,代码执行异步读取并将其转换为XDocument-writer上的同步写入。该代码受到XmlWriter.WriteNodeAsync()工作方式的启发。由于编写者构建了一个内存中的DOM,这实际上比实际执行异步写入更好。
public static async Task<XDocument> LoadAsync(Stream stream, LoadOptions loadOptions) {
    using (var reader = XmlReader.Create(stream, new XmlReaderSettings() {
            DtdProcessing = DtdProcessing.Ignore,
            IgnoreWhitespace = (loadOptions&LoadOptions.PreserveWhitespace) == LoadOptions.None,
            XmlResolver = null,
            CloseInput = false,
            Async = true
    })) {
        var result = new XDocument();
        using (var writer = result.CreateWriter()) {
            do {
                switch (reader.NodeType) {
                case XmlNodeType.Element:
                    writer.WriteStartElement(reader.Prefix, reader.LocalName, reader.NamespaceURI);
                    writer.WriteAttributes(reader, true);
                    if (reader.IsEmptyElement) {
                        writer.WriteEndElement();
                    }
                    break;
                case XmlNodeType.Text:
                    writer.WriteString(await reader.GetValueAsync().ConfigureAwait(false));
                    break;
                case XmlNodeType.CDATA:
                    writer.WriteCData(reader.Value);
                    break;
                case XmlNodeType.EntityReference:
                    writer.WriteEntityRef(reader.Name);
                    break;
                case XmlNodeType.ProcessingInstruction:
                case XmlNodeType.XmlDeclaration:
                    writer.WriteProcessingInstruction(reader.Name, reader.Value);
                    break;
                case XmlNodeType.Comment:
                    writer.WriteComment(reader.Value);
                    break;
                case XmlNodeType.DocumentType:
                    writer.WriteDocType(reader.Name, reader.GetAttribute("PUBLIC"), reader.GetAttribute("SYSTEM"), reader.Value);
                    break;
                case XmlNodeType.Whitespace:
                case XmlNodeType.SignificantWhitespace:
                    writer.WriteWhitespace(await reader.GetValueAsync().ConfigureAwait(false));
                    break;
                case XmlNodeType.EndElement:
                    writer.WriteFullEndElement();
                    break;
                }
            } while (await reader.ReadAsync().ConfigureAwait(false));
        }
        return result;
    }
}

2

首先,该任务不是异步运行的。您需要使用内置的异步IO命令或自己在线程池上启动一个任务。例如:

public static Task<XDocument> LoadAsync
 ( String path
 , LoadOptions loadOptions = LoadOptions.PreserveWhitespace
 )
{
    return Task.Run(()=>{
     using (var stream = File.OpenText(path))
        {
            return XDocument.Load(stream, loadOptions);
        }
    });
}

如果您使用Parse的流版本,则不会得到临时字符串。


3
好的,以下是我在问题最后评论中概述的内容。这将使用线程池线程来驱动隐含需要的I / O,因为XDocument通过流进行处理。而那个I / O本身会间歇性地阻塞任务的工作线程。看起来在缺乏真正的XDocument.LoadAsync()实现的情况下,这是最好的选择,该实现在内部使用适当的异步I/O指令。我没有看到显式调用File.OpenText的任何优势。最好只调用XDocument.Load(path)。 - Hydrargyrum
如果您在服务器上并行读取数万个XDocuments,您可能会担心从线程池中窃取线程而不是使用真正的异步IO,但这真的是一个问题吗? - bradgonesurfing
1
可能不是。因此我评论说这可能已经足够好了。我还是点了赞并接受了。 - Hydrargyrum
嘿,'return await Task.Run' 中的 'await' 怎么了? - jeancallisti
@jeancallisti 这里不需要使用await,因为返回的任务是方法中的最后一个处理。但是,由于没有await关键字,方法定义中也不应该有async关键字。 - Rhaokiel
@Rhaokiel 已修复。 - bradgonesurfing

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