C# 并行与。线程代码性能

Posted

技术标签:

【中文标题】C# 并行与。线程代码性能【英文标题】:C# Parallel Vs. Threaded code performance 【发布时间】:2010-08-26 06:32:44 【问题描述】:

我一直在测试 System.Threading.Parallel 与 Threading 的性能对比,我很惊讶地发现 Parallel 完成任务的时间比 threading 长。我确信这是由于我对 Parallel 的了解有限,我才刚刚开始阅读。

我想我会分享一些sn-ps,如果有人可以向我指出并行代码与线程代码相比运行速度较慢。还尝试运行相同的比较来查找素数,发现并行代码的完成时间比线程代码晚得多。

public class ThreadFactory

    int workersCount;
    private List<Thread> threads = new List<Thread>();

    public ThreadFactory(int threadCount, int workCount, Action<int, int, string> action)
    
        workersCount = threadCount;

        int totalWorkLoad = workCount;
        int workLoad = totalWorkLoad / workersCount;
        int extraLoad = totalWorkLoad % workersCount;

        for (int i = 0; i < workersCount; i++)
        
            int min, max;
            if (i < (workersCount - 1))
            
                min = (i * workLoad);
                max = ((i * workLoad) + workLoad - 1);
            
            else
            
                min = (i * workLoad);
                max = (i * workLoad) + (workLoad - 1 + extraLoad);
            
            string name = "Working Thread#" + i; 

            Thread worker = new Thread(() =>  action(min, max, name); );
            worker.Name = name;
            threads.Add(worker);
        
    

    public void StartWorking()
    
        foreach (Thread thread in threads)
        
            thread.Start();
        

        foreach (Thread thread in threads)
        
            thread.Join();
        
    

这是程序:

Stopwatch watch = new Stopwatch();
watch.Start();
int path = 1;

List<int> numbers = new List<int>(Enumerable.Range(0, 10000));

if (path == 1)

    Parallel.ForEach(numbers, x =>
    
        Console.WriteLine(x);
        Thread.Sleep(1);

    );

else

    ThreadFactory workers = new ThreadFactory(10, numbers.Count, (min, max, text) => 

        for (int i = min; i <= max; i++)
        
            Console.WriteLine(numbers[i]);
            Thread.Sleep(1);
        
    );

    workers.StartWorking();


watch.Stop();
Console.WriteLine(watch.Elapsed.TotalSeconds.ToString());

Console.ReadLine();

更新:

考虑到锁定:我尝试了以下sn-p。同样的结果,Parallel 似乎完成得慢得多。

路径 = 1; 雪灵 = 10000000;

    List<int> numbers = new List<int>();

    if (path == 1)
    
        Parallel.For(0, cieling, x =>
        
            lock (numbers)
            
                numbers.Add(x);    
            

        );
    

    else
    
        ThreadFactory workers = new ThreadFactory(10, cieling, (min, max, text) =>
        

            for (int i = min; i <= max; i++)
            
                lock (numbers)
                
                    numbers.Add(i);    
                                       

            
        );

        workers.StartWorking();
    

更新 2: 只是我的机器有四核处理器的快速更新。所以 Parallel 有 4 个内核可用。

【问题讨论】:

这里实际上有一个类似问题的好答案:***.com/questions/1116604/… 您不应该锁定 ForEach,它会在内部执行此操作。但是使用 ReaderWriterLockSlim 会再次加快速度;) 将您的 ThreadFactory 设置为 2 个线程,并将 Parallel.For 上的最大并发设置为 2,摆脱 Console.WriteLine 并做一些更合适的事情。现在他们如何比较?尝试 3 和 3; 4和4; ...在某些时候 Parallel.ForEach 将决定它分配了足够的线程,并且分配的线程数将小于您告诉它的最大值,但至少到那时您将使用 same 数量比较时间线程。 @Hightechrider:好吧,就投入实际工作量而言,正如我在问题中提到的那样,我确实针对查找素数进行了测试,这也是处理器密集型的,始终显示 100% 的活动,并发现ThreadFactory 的运行速度快了 waaaaaayyy。试试看..我什至尝试将线程数设置为 2,3 等。结果相同。 @Mikael:即使没有锁定它,它也运行缓慢。 【参考方案1】:

参考 Reed Copsey Jr 的 blog post:

Parallel.ForEach 但是有点复杂。使用通用 IEnumerable 时,需要处理的项目数事先不知道,必须在运行时发现。此外,由于我们无法直接访问每个元素,调度程序必须枚举集合来处理它。 由于 IEnumerable 不是线程安全的,它必须在枚举时锁定元素,为要处理的每个块创建临时集合,并将其安排出来

锁定和复制可能会使 Parallel.ForEach 花费更长的时间。 ForEach 的分区和调度程序也可能会影响并产生开销。我测试了你的代码并增加了每个任务的睡眠,然后结果更接近了,但 ForEach 仍然更慢。

[编辑 - 更多研究]

我在执行循环中添加了以下内容:

if (Thread.CurrentThread.ManagedThreadId > maxThreadId)
   maxThreadId = Thread.CurrentThread.ManagedThreadId;

这在我的机器上显示的是,与使用当前设置的另一个线程相比,ForEach 使用的线程少了 10 个。如果您想从 ForEach 中获得更多线程,则必须使用 ParallelOptions 和 Scheduler。

见Does Parallel.ForEach limits the number of active threads?

【讨论】:

有趣...让我尝试与启用锁定的列表上的插入进行比较。谢谢。 再次编辑 :) 归结为正在使用的线程数。 我刚读到Reed的那篇博客——这个问题中使用的分区是他所说的最简单和幼稚的分区。这使它成为一个非常好的可行性测试。还记得 OpenMP 吗?一个库必须证明很多东西才能真正被信任才能为任何真实的事情进行并行化。 @ZXX :我只是把 N 个项目拆分到不同线程的最基本任务扔给它,但它仍然不能很好地工作,不知道会出现多复杂的场景。跨度> 【参考方案2】:

我想我可以回答你的问题。首先,您没有写出您的系统有多少个内核。如果您正在运行双核,则在 Thread 示例中使用 10 个线程时,只有 4 个线程可以使用 Parallel.For。更多线程会更好地工作,因为您正在运行的任务(打印 + 短睡眠)是一个非常短的线程任务,与任务相比,线程开销非常大,我几乎可以肯定,如果您编写相同的代码而没有线程它会工作得更快。

这两种方法的工作原理几乎相同,但如果您提前创建所有线程,您可以节省很多,因为 Parallel.For 使用任务池,这会增加一些移动开销。

【讨论】:

+1:问题是苹果与橙子的总比较,因为它使用不同数量的线程。 Console.WriteLine 对于测试用例来说也是一个糟糕的选择。 我有一个四核。 @Hightechrider 我什至通过寻找素数对其进行了测试。结果相同。请尝试我的代码示例并查看结果。 再次提醒 - Parallel-s 的唯一承诺是“比手动调度更好”。没有它,除了作为一种可能的方便语法外,它没有多大用处。【参考方案3】:

关于 Threading.Parallel 的比较不是很公平。您告诉您的自定义线程池它需要 10 个线程。 Threading.Parallel 不知道它需要多少线程,因此它会尝试在运行时适应当前 CPU 负载等因素。由于测试中的迭代次数足够小,因此您可以对这个线程数进行自适应惩罚。为 Threading.Parallel 提供相同的提示会使其运行得更快:

int workerThreads; int completionPortThreads; ThreadPool.GetMinThreads(out workerThreads, out completionPortThreads); ThreadPool.SetMinThreads(10, completionPortThreads);

【讨论】:

Thnx...我会试试看它是否有所作为。 不要忘记 Parallel-s 的唯一承诺是“比手动更好”进行调度。不过,对于 null 案例来说是一件好事 - 再喂一个勺子:-)【参考方案4】:

这是合乎逻辑的:-)

这将是历史上第一次添加一层(或两层)代码来提高性能。当您使用便利库时,您应该期望付出代价。顺便说一句,您还没有发布数字。必须发布结果:-)

为了让 Parallel-s 更失败(或有偏见:-),请将列表转换为数组。

然后为了让它们完全不公平,您自己拆分工作,制作仅包含 10 个项目的数组,并完全将动作喂给 Parallel。你当然正在做 Parallel-s 承诺为你做的工作,但它肯定是一个有趣的数字:-)

顺便说一句,我刚刚阅读了 Reed 的博客。这个问题中使用的分区是他所说的最简单、最幼稚的分区。 这确实使它成为一个非常好的消除测试。您仍然需要检查零工作案例才能知道它是否完全被冲洗掉了。

【讨论】:

哈哈......好吧,如果我正在做所有的工作,那么它会破坏点仪式吗? 它为您提供测量值,告诉您 Parallel-s 在不做任何工作时是否至少可以更快 - 将其视为一个消除问题:-) 至少那部分可以解决很好,那么它们作为便利功能可能是可行的。如果不是 - 您很早就发现要避免使用哪个 dll。 不确定我是否准备好放弃它,应该有更好的优化,我在并行调用时缺少的设置。 我会告诉你你错过了什么,但不想破坏你的乐趣:-) 调度是 NP 完全问题 => 任何承诺在一般情况下这样做的软件都是纯粹的妄想。专门的软件(例如 IIS 和 CUDA 调度程序)为特殊和明确定义的类别提供服务。

以上是关于C# 并行与。线程代码性能的主要内容,如果未能解决你的问题,请参考以下文章

隐式线程与显式线程性能[关闭]

C# 并行和多线程编程——认识和使用Task

C#并行编程:.NET线程池

C# Task总结(异步操作+并行)

C#多线程之线程池篇2

5天玩转C#并行和多线程编程