为什么在本例中线程会增加时间(降低性能)

本文关键字:时间 性能 增加 线程 为什么 | 更新日期: 2023-09-27 18:21:42

此代码:

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

为什么在本例中线程会增加时间(降低性能)

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

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

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


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

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

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

  • 硬件本身。SSD没有磁盘臂,因此文件访问速度更快
  • 物理文件位置
  • 文件碎片
  • 抛光。磁盘缓冲系统有助于读取连续的块,这在您必须将臂移动到另一个位置的情况下没有任何帮助

我的建议是:如果性能是你的主要目标,那么尽量避免在多个线程中进行多次读/写。

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

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

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

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

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

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

这一点需要注意,因为工作花费的时间越长,简单线程就越有好处,因为它可以确保在进行计算时磁盘没有空闲。


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

  1. 您正在创建90000个线程-这很昂贵,而且没有必要
  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的默认设置并不总是针对您正在处理的场景进行优化,但它们提供了比运行90000个线程更好的基础!您还将注意到,在这种情况下,我不必进行任何锁定,也不必处理关闭-因为提供的API已经为我处理了这一点。

原因是两倍

  1. 创建线程是expensive,因为它需要花费大量的时间
  2. 您正在锁定obj,这实际上确保了在本例中一次只能运行一个线程,因此您实际上并不是以多线程的方式运行的

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