XDocument 到 DataTable 异步

XDocument to DataTable async

我正在将 100-500 XML 个文件中的数据加载到 DataTable 中以填充 Infragistics UltraGrid。这些文件小至 100K,大至 2MB。我一直在研究加快加载时间并考虑异步的方法,但数据table不是线程安全的。

从流程上看,加载xdoc大约花费了2/3的时间,另外1/3在读取xdoc和添加数据到table.

有没有一种方法可以使用异步并以某种方式加载下一个 XDocument,同时读取前一个并将其加载到数据 table 中?我已经看过,但我没有看到一个很好的方法来做到这一点。我应该考虑其他一些策略吗?

这是我正在做的事情的简化版本:

private void openXMLs(){
    
    OpenFileDialog openFileDialog1 = new OpenFileDialog
    {
        Title = "Browse XML files",
        CheckFileExists = true,
        CheckPathExists = true,
        DefaultExt = "xml",
        Filter = @"XML files (*.xml)|*.xml",
        FilterIndex = 2,
        RestoreDirectory = true,
        ReadOnlyChecked = true,
        ShowReadOnly = true,
        Multiselect = true
    };

    if (openFileDialog1.ShowDialog() == DialogResult.OK)
    {
        foreach (String file in openFileDialog1.FileNames)
        {
            XDocument xdoc = XDocument.Load(file)
            loadData(xdoc)
        }
    }
}
        
private void loadData(XDocument xdoc){
        
    var query = xdoc.Descendants("root").AsEnumerable()
    .Select(c => new
    {
        id = c.Element("ID").Value,
        firstname = c.Element("firstname").Value,
        lastname = c.Element("lastname").Value,
        state = c.Element("state").Value,

    });
    
    foreach (var item in query)
    {
        _dt.Rows.Add(
            item.id,
            item.firstname,
            item.lastname,
            item.state
        );
    }
}

当然,这个问题没有唯一的答案。您可以通过多种方式完成,这里是一种:

private async Task openXMLs() {
    
    // .. dialog ..

    var loadTasks = new List<Task>();        
    if (openFileDialog1.ShowDialog() == DialogResult.OK)
    {
        foreach (String file in openFileDialog1.FileNames)
        {
            var xmlFile = XmlReader.Create(file);
            XDocument xdoc = await XDocument.LoadAsync(xmlFile, LoadOptions.None, CancellationToken.None);
            loadTasks.Add(Task.Run(() => loadData(xdoc));
        }
    }

    await Task.WhenAll(loadTasks);
}

但是,数据表不是线程安全的,您需要锁定对它的访问。这也不保证添加文档的顺序。

private void loadData(XDocument xdoc){
    
    var query = xdoc.Descendants("root").AsEnumerable()
    .Select(c => new
    {
        id = c.Element("ID").Value,
        firstname = c.Element("firstname").Value,
        lastname = c.Element("lastname").Value,
        state = c.Element("state").Value,

    });
    
    lock(_dt) {
        foreach (var item in query)
        {
            _dt.Rows.Add(
                item.id,
                item.firstname,
                item.lastname,
                item.state
             );
        }
    }
}

这将在下一个从磁盘读取时加载当前文档。

您可以从文件系统并行加载下一个 XDocument,同时将上一个文档加载到 DataTable,方法是使用如下所示的 ParallelForEachPairwise 扩展方法:

/// <summary>
/// Invokes two consecutive actions on each element in the source sequence.
/// The first action is invoked in parallel with the second action for the
/// previous element, and with fetching the sequence's next element.
/// </summary>
public static void ParallelForEachPairwise<TSource, TResult>(
    this IEnumerable<TSource> source,
    Func<TSource, TResult> offloadedAction,
    Action<TResult> synchronousAction)
{
    using var enumerator = source.GetEnumerator();
    if (!enumerator.MoveNext()) return;
    Task<TResult> task = Offload(enumerator.Current);
    try
    {
        while (enumerator.MoveNext())
        {
            TResult result = task.GetAwaiter().GetResult();
            task = Offload(enumerator.Current);
            synchronousAction(result);
        }
        synchronousAction(task.GetAwaiter().GetResult());
    }
    finally { task.GetAwaiter().GetResult(); } // Prevent fire-and-forget

    Task<TResult> Offload(TSource item) => Task.Run(() => offloadedAction(item));
}

ParallelForEachPairwise 接受两个委托作为参数。第一个委托 (offloadedAction) 通过 Task.Run 方法卸载到 ThreadPool。第二个委托 (synchronousAction) 在当前线程上同步调用。这些委托彼此并行调用,但每个单独委托的重复调用是顺序的。所以你不必担心线程安全,前提是每个委托不会产生对另一个委托可见的副作用。

用法示例:

openFileDialog1.FileNames.ParallelForEachPairwise(file =>
{
    return XDocument.Load(file);
}, xdoc =>
{
    loadData(xdoc);
});

...或更简洁:

openFileDialog1.FileNames.ParallelForEachPairwise(XDocument.Load, loadData);