C#中foreach循环的性能优化

7
我有一个方法:
IList<string> pages = new List<string>();
foreach (var node in nodes)
{
    try
    {
        string temp = DoSomeComplicatedModificationOnNode(node);
        if (temp.ToLower().Contains(path))
        {
            pages.Add(node.Title);
        }
    }
    catch (Exception)
    {
        continue;
    }
}

在某些情况下,DoSomeComplicatedModificationOnNode()会引发异常,因此使用try {} catch块 - 我可以跳过引发异常的项。节点数包含数千个项目,每个项目有几个属性。如何优化此循环?我考虑使用Parallel.Foreach,但以下代码给出了错误“缺少当前主体”:

IList<string> pages = new List<string>();
Parallel.ForEach(pageNodes, node =>
{
    try
    {
        string temp = DoSomeComplicatedModificationOnNode(node);
        if (temp.ToLower().Contains(path))
        {
            pages.Add(node.Title);
        }
    }
    catch (Exception)
    {
    }
});

5
你能复制一下使用Parallel对象时出现的完整错误信息吗? - Nahuel Ianni
1
捕获“异常”并不是一个好主意,你应该捕获正确的异常。 - Sayse
据我理解,在DoSomeComplicatedModificationOnNode方法中的代码请求服务器? - igofed
1
你的问题是什么?比如说:如果DoSOmeCOmplicatedMdificationnNode没有使用太多性能,那么循环开销就很小。使用分析器找出正确或错误的地方。 - TomTom
顺便提一下:你可能想使用 ToLowerInvariant,而不是 ToLower - CodesInChaos
5个回答

10
在C#中,泛型列表不是线程安全的,因此您不能在并行循环中添加项目。
我建议使用另一个类,如ConcurrentBag,ConcurrentStack或ConcurrentQueue。
var pages = new ConcurrentBag<string>();
Parallel.ForEach(pageNodes, node =>
{
    try
    {
        string temp = DoSomeComplicatedModificationOnNode(node);
        if (temp.ToLower().Contains(path))
            pages.Add(node.Title);
    }
    catch (Exception)
    {
        throw;
    }
});

请记住,平行任务是无序的,如果您想要一个顺序,您将需要在Parallel中使用索引。列表仅适用于读取。

System.Threading.Tasks.Parallel.For(0, pageNodes.Count, index =>
{
    string node = pageNodes[index];

    try
    {
        string temp = DoSomeComplicatedModificationOnNode(node);
        if (temp.ToLower().Contains(path))
            pages.Add(MyPage(index, node.Title));
    }
    catch (Exception)
    {
        throw;
    }
});

如果性能很重要,避免同步并为每个线程使用单独的列表(并在最后合并)可能会提高速度,但我不认为我们可以在Parallel.ForEach中轻松实现这一点。 - Voo

3
我建议使用PLINQ来实现这样的目的。Parallel LINQ是LINQ的并行化实现,并具有相同的操作集合。使用PLINQ编写的代码遵循函数式规则 - 没有任何更新,只是在并行模式下映射当前列表。它可以通过在不同线程中运行映射程序,然后将结果汇集到一个“数据集”中来增加您的性能。当然,它只能在您拥有少量核心的CPU的情况下提高性能(但通常我们都有几个核心)。
以下是一个例子:
    private static void Main(string[] args)
    {
        var result =
            GenerateList()
                .AsParallel()
                .Select(MapToString)
                .Where(x => !String.IsNullOrWhiteSpace(x))
                .ToList();

        Console.ReadKey();
    }

    private const string Path = "1";
    private static string MapToString( int node)
    {
        //Console.WriteLine("Thread id: {0}", Thread.CurrentThread.ManagedThreadId);
        try
        {
            string temp = DoSomeComplicatedModificationOnNode(node);
            if (temp.ToLower().Contains(Path))
            {
                return temp;
            }
        }
        catch (Exception)
        {
            return null;
        }

        return null;
    }
    private static IEnumerable<int> GenerateList()
    {
        for (var i=0; i <= 10000; i++)
            yield return i;
    }

    private static string DoSomeComplicatedModificationOnNode(int node)
    {
        return node.ToString(CultureInfo.InvariantCulture);
    }

2

List<T>在大多数情况下不是线程安全的。可以查看线程安全的集合,例如ConcurrentBag<T>。


11
无知的评论。存在Parallel.ForEach并不能说明列表的并行性。List<T>对于插入操作不是线程安全的,这确实是一个问题。Parallel.ForEach有许多用途——在使用非线程安全容器的代码块上滥用它不是一个好主意。 - TomTom
这是一个有效的错误,但我不明白如何使用List<T>可能会导致类似于“缺少当前主体”的问题。 - CodesInChaos

0

不,使用多个线程可能不会使您的循环更快。您的代码中有三个明显的错误,这使其运行变慢。

  1. pages列表将被多次重新分配。
  2. 调用ToLower()会在每次运行时创建一个临时字符串。
  3. DoSomeComplicatedModificationOnNode可能会抛出异常。

请参见下面的修正版本。

// Give the list an initial capacity (best guess), avoiding re-allocations.
var pages = new List<string>(nodes.Length);
foreach (var node in nodes)
{
    string temp = DoSomeComplicatedModificationOnNode(node, out var error);
    if (error != null)
    {
        continue;
    }

    // IndexOf() allows for allocation-free case insensitive string search.
    if (temp.IndexOf(path, StringComparison.CurrentCultureIgnoreCase) >= 0)
    {
        pages.Add(node.Title);
    }
}

0

真正的性能问题在于你正在捕获异常,尝试使用变量通知结果。

对于长时间运行的方法,应该使用异步(asyc/await)方法。

要小心并行操作(在LINQ中使用AsParallel()),因为资源有限,当你最不希望时可能会没有内存。而且你的代码必须是线程安全的(List不是线程安全的)。

我敢打赌,你不会比以下代码表现更好:

var pages = nodes.Select(x => new { Status = DoSomeComplicatedModificationOnNode(x), Node = x })
    .Select(x => x?.Result)
    .Where(x => x.Status.IsCorrect && x.Status.ToLowerInvariant().Contains(path))
    .Select(x => x.Node.Title)
    .ToList();

或者使用异步 + LINQ:

var pages = nodes.Select(async x =>
{
    return new { Status = await DoSomeComplicatedModificationOnNode(x), Node = x };
})
.Select(x => x?.Result)
.Where(x => x.Status.IsCorrect && x.Status.ToLowerInvariant().Contains(path))
.Select(x => x.Node.Title)
.ToList();

在调用 ToList 之前,整个查询不会被执行。


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