【问题标题】:Load XDocument asynchronously异步加载 XDocument
【发布时间】:2021-01-21 20:36:58
【问题描述】:

我想将大型 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()

我想这是因为临时字符串变量用于将原始 XML 保存在内存中以供 XDocument 解析。想必在同步场景下,XDocument.Load() 能够流式传输源文件,而永远不需要创建一个巨大的 String 来保存整个文件。

有什么方法可以两全其美?使用完全异步 I/O 加载 XDocument,而不需要创建大的临时字符串?

【问题讨论】:

  • 也许你应该使用XDocument.Load(stream)
  • 如何使加载操作异步?
  • 好吧,它本身不会,但它会消除你在这里拥有的字符串变量,并希望 OOM 异常。
  • @DavidG 这就是 OP 所说的他们已经这样做了。但是他们需要异步进行操作,而不是同步。
  • 等待this 或尝试自己做。

标签: c# asynchronous async-await linq-to-xml


【解决方案1】:

【讨论】:

    【解决方案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;
        }
    }
    

    【讨论】:

      【解决方案3】:

      首先,任务不是异步运行的。您需要使用内置的异步 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 的 stream version,那么您不会得到临时字符串。

      【讨论】:

      • 好的。这就是我在对这个问题的最后评论中概述的内容。因此,这将使用线程池线程来驱动隐式所需的 I/O,因为 XDocument 会在流中咀嚼它的方式。并且 I/O 本身会偶尔阻塞 Task 的工作线程。如果没有真正的 XDocument.LoadAsync() 实现,它在后台使用适当的异步 I/O 指令,看起来这是可以做到的最好的。我看不出显式调用 File.OpenText 有什么好处。也可以只调用 XDocument.Load(path)
      • 如果您在服务器上并行读取成千上万个 XDocument,您可能会担心从线程池中窃取线程而不是使用真正的异步 IO,但这真的是一个问题吗?
      • 可能不会。因此我的评论是它可能已经足够好了。无论如何我都赞成并接受了
      • 嘿,'return await Task.Run' 中的 'await' 发生了什么?
      • @jeancallisti 这里不需要等待,因为返回的任务是方法中的最后一个处理。但是,因为没有 await 关键字,所以方法定义中也不应该有 async 关键字。
      猜你喜欢
      • 2021-11-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多