【问题标题】:Multithreading slower than Singlethreading多线程比单线程慢
【发布时间】:2012-09-05 14:36:35
【问题描述】:

我有以下代码(控制台应用程序的“Program.cs”的完整内容)。 'countUp' 到 'countUp4' 的单线程执行需要 13 秒,多线程执行需要 21 秒..

我有一个 Intel Core i5-2400 @ 3.10 GHz、8 GB 内存、Windows 7 64 位。那么为什么多线程执行比单线程慢呢?

多线程是否只对不阻塞简单 c# 应用程序的主程序有用?多线程在什么时候给我带来了执行速度的优势?

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace ConsoleApplication1
{
    class Program
    {
        static int counter = 0;
        static int counter2 = 0;
        static int counter3 = 0;
        static int counter4 = 0;

        static void Main(string[] args)
        {
            Console.WriteLine("Without multithreading:");
            Console.WriteLine("Start:" + DateTime.Now.ToString());

            countUp();
            countUp2();
            countUp3();
            countUp4();

            Console.WriteLine("");
            Console.WriteLine("With multithreading:");
            Console.WriteLine("Start:" + DateTime.Now.ToString());

            Thread thread1 = new Thread(new ThreadStart(countUp));
            thread1.Start();
            Thread thread2 = new Thread(new ThreadStart(countUp2));
            thread2.Start();
            Thread thread3 = new Thread(new ThreadStart(countUp3));
            thread3.Start();
            Thread thread4 = new Thread(new ThreadStart(countUp4));
            thread4.Start();

            Console.Read();
        }

        static void countUp()
        {
            for (double i = 0; i < 1000000000; i++)
            {
                counter++;
            }

            Console.WriteLine(counter.ToString());
            Console.WriteLine(DateTime.Now.ToString());
        }

        static void countUp2()
        {
            for (double i = 0; i < 1000000000; i++)
            {
                counter2++;
            }

            Console.WriteLine(counter2.ToString());
            Console.WriteLine(DateTime.Now.ToString());
        }

        static void countUp3()
        {
            for (double i = 0; i < 1000000000; i++)
            {
                counter3++;
            }

            Console.WriteLine(counter3.ToString());
            Console.WriteLine(DateTime.Now.ToString());
        }

        static void countUp4()
        {
            for (double i = 0; i < 1000000000; i++)
            {
                counter4++;
            }

            Console.WriteLine(counter4.ToString());
            Console.WriteLine(DateTime.Now.ToString());
        }
    }
}

【问题讨论】:

  • 我很惊讶这甚至可以工作......你不是从后台访问 UI 线程吗?这是实际的代码吗?
  • 这里没什么普通的。控制台可能(这只是我的猜测)需要同步,因此所有线程都在相互等待。多线程自动更快,这是一个普遍存在的错误信息。你必须处理同步、上下文切换等等。
  • 首先,DateTime.Now 是一种糟糕的计时方式,请改用Stopwatch 类。其次,并非所有代码都能从多线程中受益。
  • @Killercam 什么 UI 线程?这是一个控制台应用程序
  • 其实Console已经同步了。

标签: c# multithreading performance


【解决方案1】:

这是您可能看不到的原因:false sharing,因为这 4 个 int 在内存中都并排放置。

更新 - 往年的 MSDN 杂志现在只能以 .chm 文件的形式提供 - 所以你必须抓住 'October 2008' edition of the MSDN Mag from here 并且,下载后,你必须记得右键单击并“取消阻止”文件在打开文件之前从 Windows 资源管理器中的文件属性对话框(其他操作系统可用!)。您正在寻找由 Stephen Toub、Igor Ostrovsky 和 ​​Huseyin Yildiz 撰写的名为“.Net Matters”的专栏

这篇文章(阅读全部内容 - 非常棒)展示了内存中并排的值如何在更新时最终导致阻塞,因为它们都位于同一缓存行上。这是您无法从 .Net 代码中禁用的非常低级的阻塞。但是,您可以强制将数据间隔得更远,以便保证或至少增加每个值位于不同缓存行上的可能性。

这篇文章使用了数组 - 但它可能会影响到你。

要遵循以下建议,您可以通过稍微更改代码来证明/反驳这一点:

class Program 
{ 
    class CounterHolder {
       private int[] fakeInts = new int[1024];
       public int Value = 0;
    }
    static CounterHolder counter1 = new CounterHolder(); 
    static CounterHolder counter2 = new CounterHolder(); 
    static CounterHolder counter3 = new CounterHolder(); 
    static CounterHolder counter4 = new CounterHolder(); 

然后修改你的线程函数来操作每个计数器持有者上的公共字段Value

我已经使这些数组比他们需要的要大得多,希望它能证明它更好:)

【讨论】:

  • 应该很容易证明,@KaiHartmann,你为什么不把每个 int 放在每个方法的本地增量中,看看它是否有所作为。
  • 是的,刚刚试过。如果您将计数器设为本地,则多个线程会更快。
  • @JohnnyHK 更新了另一个应该有助于证明这一点的想法。
  • +1 好发现!我还写了一篇关于虚假分享的文章:here
  • 指向 MSDN 的链接已损坏 - 您有更新的吗?
【解决方案2】:

Andreas Zaltan 就是答案。拿代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Diagnostics;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        //static int counter = 0;
        //static int counter2 = 0;
        //static int counter3 = 0;
        //static int counter4 = 0;

        class CounterHolder
        {
            private int[] fakeInts = new int[1024];
            public int Value = 0;
        }
        static CounterHolder counter1 = new CounterHolder();
        static CounterHolder counter2 = new CounterHolder();
        static CounterHolder counter3 = new CounterHolder();
        static CounterHolder counter4 = new CounterHolder(); 

        static void Main(string[] args)
        {
            Console.WriteLine("Without multithreading:");
            Console.WriteLine("Start: " + DateTime.Now.ToString());

            Stopwatch sw = new Stopwatch();
            sw.Start();

            countUp();
            countUp2();
            countUp3();
            countUp4();

            sw.Stop();
            Console.WriteLine("Time taken = " + sw.Elapsed.ToString());

            Console.WriteLine("\nWith multithreading:");
            Console.WriteLine("Start: " + DateTime.Now.ToString());
            sw.Reset();
            sw.Start();

            Task task1 = Task.Factory.StartNew(() => countUp());
            Task task2 = Task.Factory.StartNew(() => countUp2());
            Task task3 = Task.Factory.StartNew(() => countUp3());
            Task task4 = Task.Factory.StartNew(() => countUp4());
            var continuation = Task.Factory.ContinueWhenAll(new[] { task1, task2, task3, task4 }, tasks =>
            {
                Console.WriteLine("Total Time taken = " + sw.Elapsed.ToString());
            });
            Console.Read();
        }

        static void countUp()
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            for (double i = 0; i < 1000000000; i++)
                counter1.Value++;
            sw.Stop();
            Console.WriteLine("Task countup took: " + sw.Elapsed.ToString());
        }

        static void countUp2()
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            for (double i = 0; i < 1000000000; i++)
                counter2.Value++;
            sw.Stop();
            Console.WriteLine("Task countUP2 took: " + sw.Elapsed.ToString());
        }

        static void countUp3()
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            for (double i = 0; i < 1000000000; i++)
                counter3.Value++;
            sw.Stop();
            Console.WriteLine("Task countUP2 took: " + sw.Elapsed.ToString());
        }

        static void countUp4()
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            for (double i = 0; i < 1000000000; i++)
                counter4.Value++;
            sw.Stop();
            Console.WriteLine("Task countUP2 took: " + sw.Elapsed.ToString());
        }
    } 
}

用整数运行它,你会得到多线程版本的运行速度稍微慢一些。

Serial: 13.88s
Multi-threaded: 14.01

使用上面的建议运行它,您会得到以下结果

为了清楚起见,我已经发布了这个......

【讨论】:

    【解决方案3】:

    我用 StopWatch 重写了您的代码。在我的电脑上,多线程比单线程快(以下几倍)。

    此外,您需要在线程上调用方法 Join 以确保它们在退出程序之前完成。

    没有多线程时经过的时间:: 00:00:21.6897179

    多线程经过的时间:: 00:00:14.7893703

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading;
    using System.Diagnostics;
    
    namespace ConsoleApplication1
    {
        class Program
        {
            static int counter = 0;
            static int counter2 = 0;
            static int counter3 = 0;
            static int counter4 = 0;
    
            static void Main(string[] args)
            {
                Stopwatch stopwatch = new Stopwatch();
                stopwatch.Start();
    
                countUp();
                countUp2();
                countUp3();
                countUp4();
    
                stopwatch.Stop();
                Console.WriteLine("Time elapsed without multithreading:: {0}",
            stopwatch.Elapsed);
    
                stopwatch.Reset();
                stopwatch.Start();
    
                Thread thread1 = new Thread(new ThreadStart(countUp));
                thread1.Start();
                Thread thread2 = new Thread(new ThreadStart(countUp2));
                thread2.Start();
                Thread thread3 = new Thread(new ThreadStart(countUp3));
                thread3.Start();
                Thread thread4 = new Thread(new ThreadStart(countUp4));
                thread4.Start();
    
                thread1.Join();
                thread2.Join();
                thread3.Join();
                thread4.Join();
    
                stopwatch.Stop();
                Console.WriteLine("Time elapsed with multithreading:: {0}",
            stopwatch.Elapsed);
    
                Console.Read();
            }
    
            static void countUp()
            {
                for (double i = 0; i < 1000000000; i++)
                {
                    counter++;
                }
            }
    
            static void countUp2()
            {
                for (double i = 0; i < 1000000000; i++)
                {
                    counter2++;
                }
            }
    
            static void countUp3()
            {
                for (double i = 0; i < 1000000000; i++)
                {
                    counter3++;
                }
            }
    
            static void countUp4()
            {
                for (double i = 0; i < 1000000000; i++)
                {
                    counter4++;
                }
            }
        }
    }
    

    【讨论】:

    • 我明天会检查这个。 Andras Zoltan 的提示解决了我的问题,但也许这也是正确的......
    • 啊,我读得很快。 Join 只是为了确保线程完成......我认为它可以解决问题,但事实并非如此。所以我想知道,为什么它在你的机器上更快。没有像 Andras Zoltan 描述的那样锁定吗?顺便提一句。尽管如此,加入的部分还是有帮助的......
    • 我认为 Console.Read() 的存在是为了防止主线程提前退出,从而导致其他线程提前终止。 - 如果是这样,为什么还要加入()线程?
    【解决方案4】:

    我不是多线程方面的专家,但我认为您在那里所做的基本上只是将工作从 UI 线程中移开。

    如果您有一些长期或密集的工作要做,这绝不是一件坏事,因为它允许您为最终用户保留响应式 UI。为了更快地运行这样的事情,如果我的记忆正确地为我服务,您将需要研究并行处理。

    【讨论】:

    • 假设您的意思是“那个”的并行处理,我知道。我说这就是他想要更快地运行该代码的原因。我可能误解了你的评论,但它并没有清楚地描述“那个”是什么。
    • 我的意思是你的整个帖子都是错误的。这是一个控制台应用程序,因此没有 UI 线程。他不仅使它更具响应性,而且实际上是在并行执行单独的(无用的,但这只是一个示例)任务,因此应该加快速度。
    【解决方案5】:

    首先,使用 System.Runtime.Diagnostic 命名空间中的 StopWatch 类而不是 DateTime 进行测量。

    其次,同步执行后不清除“计数器”。

    你应该对所有线程使用并行化,比它更快!初始化新线程的成本很高。顺便说一句,你可以使用线程池。

    【讨论】:

    • 我认为我们也不能假设 TPL 更快。它仍然有调度成本。
    • 计数器不重置并不重要。它不会使代码更快/更慢;他们只是在那里消耗处理器周期。至于线程池,在给出的示例中,它在特定上下文中不应有显着差异。
    • 使用 ThreadPool 的执行时间几乎相同。
    • @KaiHartmann 那是因为线程池开始是空的,被赋予了足够的任务,需要为每个线程创建一个线程,然后永远不会重用它们中的任何一个。如果您创建了更多更小的工作单元,那么线程池可能比显式创建线程更好。
    • 是的,你是对的,在这种情况下,重置并不重要。但如果你有一个复杂的计算,那就很重要了!您应该始终使用相同的条件进行性能测试..
    【解决方案6】:

    正如 Joeb454 所说,在这种情况下,您必须寻找并行处理。 您的多线程只会减慢执行速度,因为创建新线程需要“很长时间”。

    【讨论】:

    • 创建一个线程只需几毫秒。在这种情况下,我认为这不是一个“长”时间。
    • 任务的执行时间大约为几秒。相比之下,创建线程所花费的时间将相形见绌。如果实际任务的执行时间要小得多,那么这可能是一个问题。
    猜你喜欢
    • 1970-01-01
    • 2020-08-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-21
    • 2011-03-01
    相关资源
    最近更新 更多