在这个例子中,为什么线程会增加时间(降低性能)?

时间:2013-07-09 11:10:41

标签: c# multithreading performance

此代码:

  object obj = new object { };
  Stopwatch watch = new Stopwatch();
  watch.Start();
  for (int i = 0; i < 90000; i++)
  {
      new Thread(() =>
      {
          lock (obj)
          {
              string file = new JavaScriptSerializer().Serialize(saeed);
              File.AppendAllText(string.Format(@"c:\Temp\{0}.txt", i), file);
          }
      }).Start();
  }
  watch.Stop();

运行15分钟,而这段代码:

  Stopwatch watch = new Stopwatch();
  watch.Start();
  for (int i = 0; i < 90000; i++)
  {
      {
          string file = new JavaScriptSerializer().Serialize(saeed);
          File.AppendAllText(string.Format(@"c:\Temp\{0}.txt", i), file);
      }
  }
  watch.Stop();

在45秒内运行。为什么第一个应用程序在线程化时速度要慢得多?使用线程是否是一种提高应用程序性能的技术?

更新:即使在我的线程中使用闭包概念并引用中间变量而不是i而不是使用lock,这使得线程真的异步,仍然创建这些文件需要更多超过5分钟。

  Stopwatch watch = new Stopwatch();
  watch.Start();
  for (int i = 0; i < 90000; i++)
  {
      var x = i;
      new Thread(() =>
      {
          string file = new JavaScriptSerializer().Serialize(saeed);
          File.AppendAllText(string.Format(@"c:\Temp\{0}.txt", i), file);
      }).Start();
  }
  watch.Stop();

5 个答案:

答案 0 :(得分:34)

1)您目前正在创建90000个线程,这根本没有效率。不要每次创建一个线程,而是使用线程池,因此您可以重用已创建的线程。记住创建一个线程需要一些时间和内存。

2)您使用lock锁定整个代码块,这意味着每个线程都被阻塞,直到另一个线程完成其工作。所以你基本上在这里打败了多线程的整个目的。

3)出于复杂的硬件相关原因(缓冲等等),磁盘I / O不能很好地与多线程配合使用。通常,将这部分代码多线程化并不是一个好主意。


关于磁盘I / O和多线程的评论:实际上非常复杂。

对于磁盘,磁盘臂必须移动才能在良好扇区/柱面/磁道上读/写字节。如果你同时写两个不同的文件(两个线程的情况,每个写一个不同的文件),根据磁盘上的物理文件位置,你可能会要求你的磁盘臂从一个物理位置切换到另一个物理位置很快,这会摧毁表演。在一个物理位置为第一个文件写入多个磁盘扇区,然后将磁盘臂移动到另一个位置,然后为第二个文件写入一些磁盘扇区会更有效率。比较同时复制两个文件与复制一个文件的时间,然后是另一个文件时,可以看到这种效果。

因此,对于这个非常基本的例子,性能增益/损失取决于:

  • 硬件本身。没有带SSD的磁盘臂,因此文件访问速度更快
  • 物理文件位置
  • 文件碎片
  • bufferization。 disk buffer system有助于读取连续的块,如果您不得不将手臂移动到另一个位置,则无法提供任何帮助。

我的谦虚建议:如果演出是你的主要目标,尽量避免在多个线程中进行多次读/写。

答案 1 :(得分:19)

线程可以通过为您提供更多的执行引擎来加速您的代码。但是您在第一个片段中探索了截然不同的资源限制。

第一个是机器提交90 千兆字节内存的能力。线程堆栈所需的空间。这需要一段时间,你的硬盘可能会疯狂地为这么多内存创建备份存储。 .NET有点不寻常,因为它为一个线程提交了堆栈空间,它提供了一个执行保证。你可以关闭btw的东西,app.exe.config文件中的<disableCommitThreadStack>元素应该有非常明显的效果。

您正在探索的第二个资源限制是文件系统同时修改多个文件的能力。它会受到第一个限制的极大阻碍,你正在从文件系统缓存中窃取大量RAM。当它用完空间时,你会看到这些线程都试图占用磁盘写头的效果。强制它在文件集群之间来回压缩。磁盘搜索非常缓慢,到目前为止磁盘上的最慢操作。这是一种机械操作,驱动头臂需要物理移动,这需要花费很多毫秒。您的代码很可能产生的硬页错误也会使情况变得更糟。

线程代码中的 lock 将减少这种颠簸,但不会消除它。由于内存需求量很大,您的程序可能会产生大量的页面错误。更糟糕的情况是每个线程上下文切换。当磁盘执行seek + read以满足页面调入请求时,线程将被阻塞。

嗯,通过让你这样做而不是摔倒来赞美Windows。但显然这是一个坏主意。最多使用少数个帖子。或者只有一个如果写入将使文件系统缓存饱和,那么你将避免寻求惩罚。

答案 2 :(得分:7)

我会注意到大多数答案都没有读过代码的例子。这不是关于产生一堆线程并写入磁盘,这是关于产生一堆线程,做一些工作新的JavaScriptSerializer()。序列化(saeed); 然后写入磁盘!

这一点非常重要,因为工作时间越长,简单线程提供的好处就越多,确保磁盘在计算过程中不会空闲。


它的长短是因为你写了一些简单的代码,正如其他人所解释的那样:

  1. 您正在创建90,000个线程 - 这很昂贵 不必要!
  2. 你正在锁定所有的工作,使这个单线程!
    1. 没有锁定,你会得到一个例外......这并不能让你从一个表演理念中成为一个好主意 - 它只是意味着你有错误的代码。
  3. 进入线程的一种快速简便的方法 - 虽然你仍然可以填充它,但是使用任务并行库会稍微不那么危险。例如:

    using System;
    using System.Diagnostics;
    using System.IO;
    using System.Threading.Tasks;
    
    namespace ConsoleApplication15
    {
        class Program
        {
            const int FILE_COUNT = 9000;
            const int DATA_LENGTH = 100;
            static void Main(string[] args)
            {
                if (Directory.Exists(@"c:\Temp\")) Directory.Delete(@"c:\Temp\", true);
                Directory.CreateDirectory(@"c:\Temp\");
    
                var watch = Stopwatch.StartNew();
                for (int i = 0; i < FILE_COUNT; i++)
                {
                    string data = new string(i.ToString()[0], DATA_LENGTH);
                    File.AppendAllText(string.Format(@"c:\Temp\{0}.txt", i), data);
                }
                watch.Stop();
                Console.WriteLine("Wrote 90,000 files single-threaded in {0}ms", watch.ElapsedMilliseconds);
    
                Directory.Delete(@"c:\Temp\", true);
                Directory.CreateDirectory(@"c:\Temp\");
    
                watch = Stopwatch.StartNew();
                Parallel.For(0, FILE_COUNT, i =>
                {
                    string data = new string(i.ToString()[0], DATA_LENGTH);
                    File.AppendAllText(string.Format(@"c:\Temp\{0}.txt", i), data);
                });
                watch.Stop();
                Console.WriteLine("Wrote 90,000 files multi-threaded in {0}ms", watch.ElapsedMilliseconds);
            }
        }
    }
    

    单线程版本在大约8.1秒内运行,多线程版本在大约3.8秒内运行。请注意,我的测试值与您的测试值不同。

    虽然TPL的默认设置并不总是针对您正在处理的场景进行优化,但它们提供了比运行90,000个线程更好的基础!你还会注意到,在这种情况下,我不需要做任何锁定,也不必处理闭包 - 因为提供的API已经为我处理了。

答案 3 :(得分:4)

原因是双重

  1. 创建一个帖子是expensive,因为这需要花费很多时间。
  2. 您正在锁定obj,这实际上确保在此示例中一次只能运行一个线程,因此您实际上并未以多线程方式运行。

答案 4 :(得分:3)

因为带有锁的for循环内部产生了一个线程。因此,线程是逐个执行的,而不是像第二个例子中那样同时执行。