【问题标题】:Why does threading increase time (reduce performance) in this example?为什么在这个例子中线程会增加时间(降低性能)?
【发布时间】:2013-07-09 11:10:40
【问题描述】:

这段代码:

  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 而不是使用锁,这使得线程真正异步,创建这些文件仍然需要超过 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();

【问题讨论】:

  • 如果你添加一个巨大的lock 块,那么多线程是没有意义的......
  • 因为每次迭代都会创建新线程
  • 您每次都在启动新线程。每个线程都有相同的同步。您也可以编写一个正常的 for 循环,最后再添加几个零。 CPU 旋转了很长时间,等待你的锁被释放。
  • 将线程视为“提高应用程序性能的技术”是非常危险的。
  • 去掉锁,然后用Enumerable.Range(0,90000).AsParallel().ForAll( i=&gt; { Contents of task});替换你的for

标签: c# multithreading performance


【解决方案1】:

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

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

3) 由于复杂的硬件相关原因(缓冲区...等),磁盘 I/O 不能很好地与多线程一起工作。一般来说,这部分代码多线程不是一个好主意。


关于磁盘 I/O 和多线程的 cmets:这实际上是相当复杂的。

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

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

  • 硬件本身。没有带 SSD 的磁盘臂,因此文件访问速度更快
  • 物理文件位置
  • 文件碎片
  • 缓冲。 disk buffer system 有助于读取连续的块,如果您必须将手臂移动到另一个位置,这将无济于事。

我的谦虚建议:如果性能是您的主要目标,请尽量避免在多个线程中进行多次读取/写入。

【讨论】:

  • #3 不是绝对的。事实上,并行处理多个文件是一种常见(且有效)的技巧,可用于从命令行进行快速并行处理。例如。在 Linux 中使用 xargs 对多个文件执行相同的命令
  • 取决于数据,如果您需要从 500 个文件加载 500kb 并进行繁重的 CPU 密集型处理,那么它会有所帮助,否则如果它的 500MB 并且只是移动数据,那么它可能无济于事这么多
  • 很明显,我们这里说的是500个文件
  • @PanagiotisKanavos 你是绝对正确的,但并非总是如此。但这取决于很多因素(磁盘与 SSD、文件碎片、写入块的大小......),很难期待性能提升,如果可能的话应该避免(恕我直言)除非你真的知道你是什么做多线程的I/O会有什么影响
  • 这并不是说磁盘 IO 不能很好地与多线程一起工作 - 实际上,当在一个线程中阻塞 IO 而在其他线程中执行操作或轮流进行 IO 时,多线程可能很有用。关键是您通常不会在以最高速度从多个线程写入同一张磁盘的性能上获得任何提升,因为它只是一张磁盘而且它比处理器慢得多,所以多线程确实如此不影响这里的真正瓶颈。 “缓冲区”与这一切关系不大(实际上,它们有助于提高性能)。
【解决方案2】:

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

第一个是机器提交 90 GB 内存的能力。线程堆栈所需的空间。这需要一段时间,如果您的硬盘可能正在努力为这么多内存创建备份存储,那么这需要一段时间。 .NET 有点不寻常,因为它为线程提交堆栈空间,它提供了执行保证。顺便说一句,你可以关闭一些东西,app.exe.config 文件中的&lt;disableCommitThreadStack&gt; 元素应该有非常明显的效果。

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

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

好吧,让您这样做而不是摔倒是对 Windows 的赞誉。但显然这是个坏主意。最多使用 几个 线程。或者,如果写入无论如何都会使文件系统缓存饱和,则只需一个,这样您就可以避免查找惩罚。

【讨论】:

  • 我总是设法从汉斯的帖子中学到一些东西。谢谢。
  • 在问题中提供的原始代码中,锁阻止了代码同时工作。创建了 90000 个线程,所有线程都在门口,除了一个运行代码。我希望大部分时间都花在等待线程管理上。
【解决方案3】:

我会注意到大多数答案都没有阅读示例代码。这不是关于生成一堆线程并写入磁盘,而是关于生成一堆线程,做一些工作 new JavaScriptSerializer().Serialize(saeed);then 写入磁盘!

请务必注意这一点,因为工作花费的时间越长,简单线程通过确保在计算发生时磁盘不空闲而提供的好处就越多。


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

  1. 您正在创建 90,000 个线程 - 这很昂贵并且 不必要!
  2. 您正在锁定所有工作,使这个单线程!
    1. 是的,如果没有锁,您会得到一个异常...这并不能神奇地使锁成为性能理念的好主意 - 它只是意味着您有错误的代码。

进入线程的一种快速简便的方法是使用任务并行库,这种方法的危险性稍小一些(尽管您仍然可以将其塞满)。例如:

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 已经为我处理了。

【讨论】:

  • TPL 任务(无论如何都使用线程池)在这里有什么帮助?
  • @SimonWhitehead TPL 提供多线程,在我的测试中确实提高了性能。 TPL 还提供了一系列内置功能,例如线程池,与 OP 的尝试相比,它们显着提高了性能。
  • 如果不对其进行测试,我很难相信性能提升如此之大。是的,池化将导致
  • @SimonWhitehead 我在测试代码中的表现翻了一番,重读我的答案。
  • TPL 负责使用合理数量的线程。在 CPU 密集型任务的情况下,它会使用与内核一样多的线程。
【解决方案4】:

原因有两个

  1. 创建线程是expensive,因为它需要大量的时间来完成。
  2. 您正在锁定obj,这实际上确保在此示例中一次只能运行一个线程,因此您实际上并没有以多线程方式运行。

【讨论】:

  • 如果您删除 lock 语句,则会触发异常。试试看。
  • @SaeedNamati:是的,更新我的答案。
  • 也许他需要不同的文件。
  • 他可能会遇到异常,因为在启动的线程使用它来生成文件名的那一刻,他无法控制 i 的值,并且他很可能最终让线程同时访问同一个文件时间。
【解决方案5】:

因为在带有锁的 for 循环中创建了一个线程。所以线程是一个接一个地执行,而不是像第二个例子那样同时执行。

【讨论】:

  • 如果您删除 lock 语句,则会触发异常。试试看。
  • 除了锁之外,在系统上创建 90K 线程并不容易。
  • @SaeedNamati 有什么例外?
  • 进程无法访问文件'c:\Temp\66.txt',因为它正被另一个进程使用。
  • @Saeed Neamati,这是因为您在示例中使用闭包的方式。多线程时不能这样使用它。 i 的值不断变化,并且无法保证当您在线程中访问 i 时,其值将与创建该线程时的值相同。您应该在每次迭代时删除 lock 并将 i 值复制到局部变量,然后在线程中使用此局部变量,而不是使用 i
猜你喜欢
  • 1970-01-01
  • 2018-08-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-04-19
  • 1970-01-01
  • 2021-11-19
相关资源
最近更新 更多