6

这段代码:

  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();
4

5 回答 5

34

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

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

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


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

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

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

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

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

于 2013-07-09T11:17:14.283 回答
19

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

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

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

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

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

于 2013-07-09T11:35:40.283 回答
7

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

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


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

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

于 2013-07-09T11:48:43.513 回答
4

原因有两个

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

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

于 2013-07-09T11:14:23.997 回答