Fork me on GitHub
随笔 - 184,  文章 - 0,  评论 - 117,  阅读 - 62万

让我们考虑一个简单的编程挑战:对大数组中的所有元素求和。现在可以通过使用并行性来轻松优化这一点,特别是对于具有数千或数百万个元素的巨大阵列,还有理由认为,并行处理时间应该与常规时间除以CPU核心数一样多。事实证明,这一壮举并不容易实现。我将向您展示几种并行执行此操作的方法,它们如何改善或降低性能以及以某种方式影响性能的所有细节。

简单的循环方法

复制代码
private const int ITEMS = 500000;
private int[] arr = null;

public ArrayC()
{
    arr = new int[ITEMS];
    var rnd = new Random();
    for (int i = 0; i < ITEMS; i++)
    {
        arr[i] = rnd.Next(1000);
    }
}

public long ForLocalArr()
{
    long total = 0;
    for (int i = 0; i < ITEMS; i++)
    {
        total += int.Parse(arr[i].ToString());
    }

    return total;
}

public long ForeachLocalArr()
{
    long total = 0;
    foreach (var item in arr)
    {
        total += int.Parse(item.ToString());
    }

    return total;
}
复制代码

只需要迭代循环就可以计算出结果,超级简单,这里没有用直接相加求出结果,原因是直接求出结果,发现每次基本的运行都比并行快,但是实际上,并行处理没有那么简单,所以这里的加法就简单的处理下total += int.Parse(arr[i].ToString())。现在,让我们尝试用并行性来打败数组迭代吧。

首次尝试

复制代码
private object _lock = new object();

public long ThreadPoolWithLock()
{
    long total = 0;
    int threads = 8;
    var partSize = ITEMS / threads;
    Task[] tasks = new Task[threads];
    for (int iThread = 0; iThread < threads; iThread++)
    {
        var localThread = iThread;
        tasks[localThread] = Task.Run(() =>
        {
            for (int j = localThread * partSize; j < (localThread + 1) * partSize; j++)
            {
                lock (_lock)
                {
                    total += arr[j];
                }
            }
        });
    }

    Task.WaitAll(tasks);
    return total;
}
复制代码

请注意,您必须使用localThread变量来“保存”该iThread时间点的值。否则,它将是一个随着for循环前进而变化的捕获变量。当数据最后打的时候并行已经比普通的快了,但是发现快的不多,说明还可以优化

再次优化

复制代码
public long ThreadPoolWithLock2()
{
    long total = 0;
    int threads = 8;
    var partSize = ITEMS / threads;
    Task[] tasks = new Task[threads];
    for (int iThread = 0; iThread < threads; iThread++)
    {
        var localThread = iThread;
        tasks[localThread] = Task.Run(() =>
        {
            long temp = 0;
            for (int j = localThread * partSize; j < (localThread + 1) * partSize; j++)
            {
                temp += int.Parse(arr[j].ToString());
            }

            lock (_lock)
            {
                total += temp;
            }
        });
    }

    Task.WaitAll(tasks);
    return total;
}
复制代码

增加设置临时变量,减少lock次数,发现运行效果已经有质的提高,提高了几倍。忽然想起,有个Parallel.For的方法,研究性能是否可以更快。

Parallel.For优化

复制代码
public long ParallelForWithLock()
{
    long total = 0;
    int parts = 8;
    int partSize = ITEMS / parts;
    var parallel = Parallel.For(0, parts, new ParallelOptions(), (iter) =>
    {
        long temp = 0;
        for (int j = iter * partSize; j < (iter + 1) * partSize; j++)
        {
            temp += int.Parse(arr[j].ToString());
        }

        lock (_lock)
        {
            total += temp;
        }
    });
    return total;
}
复制代码

运行结果比普通迭代快,但是没有ThreadPool快,但是觉得Parallel.For还可以继续优化,也许可以更快

Parallel.For继续优化

复制代码
public long ParallelForWithLock2()
{
    long total = 0;
    int parts = 8;
    int partSize = ITEMS / parts;
    var parallel = Parallel.For(0, parts,
        localInit: () => 0L, // Initializes the "localTotal"
        body: (iter, state, localTotal) =>
        {
            for (int j = iter * partSize; j < (iter + 1) * partSize; j++)
            {
                localTotal += int.Parse(arr[j].ToString());
            }

            return localTotal;
        },
        localFinally: (localTotal) => { total += localTotal; });
    return total;
}
复制代码

运行效果已经很快,和ThreadPool优化过的差不多,有些时候更快

避免在循环中使用Task.Run

您可以在要执行并发活动时使用任务,如果您需要高度的并行性,任务永远不是一个好的选择,始终建议避免在ASP.Net中使用线程池线程。因此,您应该避免在ASP.Net中使用Task.Run或Task.factory.StartNew。

Task.Run应始终用于CPU绑定代码。Task.Run在ASP.Net应用程序或利用ASP.Net运行时的应用程序中不是一个好选择,因为它只是将工作卸载到ThreadPool线程。如果您使用的是ASP.Net Web API,则该请求已经使用了ThreadPool线程。因此,如果在ASP.Net Web API应用程序中使用Task.Run,​​则只是通过将工作卸载到另一个工作线程来限制可伸缩性。

请注意,在循环中使用Task.Run存在缺点。如果在循环中使用Task.Run方法,则会创建多个任务 - 每个工作单元或迭代一个任务。但是,如果使用Parallel.ForEach代替在循环中使用Task.Run,​​则会创建分区程序以避免创建更多任务来执行活动而不是需要它。这可能会显着提高性能,因为您可以避免过多的上下文切换,并且仍然可以利用系统中的多个内核。

应该注意的是,Parallel.ForEach在内部使用Partitioner <T>,以便将集合分发到工作项中。顺便说一句,这种分发不会发生在项目列表中的每个任务中,而是作为批处理发生。这降低了所涉及的开销,从而提高了性能。换句话说,如果在循环中使用Task.Run或Task.Factory.StartNew,它们将为循环中的每次迭代显式创建新任务。Parallel.ForEach更有效,因为它将通过在系统中的多个核心之间分配工作负载来优化执行。

结论和总结

并行化优化肯定可以提高性能,但是这取决于很多因素,每个案例都应该进行测量和检查。
当各种线程需要通过某种锁定机制相互依赖时,性能会显着降低。

50万数据运行结果

 

其他的多线程文章

1. C#中await/async闲说

2. .NET中并行开发优化

3. C# Task.Run 和 Task.Factory.StartNew 区别

4. C#中多线程的并行处理

5. C#中多线程中变量研究

posted on   lingfeng95  阅读(4455)  评论(10编辑  收藏  举报
编辑推荐:
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
历史上的今天:
2015-06-22 面向对象编程的解释

< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5
点击右上角即可分享
微信分享提示