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

C# 在本例中,线程为什么会增加时间(降低性能)?,c#,multithreading,performance,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

此代码:

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

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

原因有两个

  • 创建线程是非常昂贵的,因为它需要花费大量的时间
  • 您正在锁定
    obj
    ,这实际上确保了在本例中一次只能运行一个线程,因此您实际上不是以多线程方式运行的 1) 您当前正在创建90000个线程,这根本没有效率。不要每次都创建线程,而是使用线程池,这样就可以重用已经创建的线程。请记住,创建线程需要一些时间和内存

    2) 您使用
    lock
    锁定整个代码块,这意味着每个线程都将被锁定,直到另一个线程完成其工作。因此,在这里,您基本上破坏了多线程的全部目的

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


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

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

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

    • 硬件本身。没有带SSD的磁盘arm,因此文件访问速度更快
    • 物理文件位置
    • 文件碎片
    • 缓冲化。该选项有助于读取连续块,如果您必须将手臂移动到其他位置,则该选项将毫无帮助

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

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

    第一个是机器提交90 GB内存的能力。线程堆栈所需的空间。这需要一段时间,你的硬盘可能会拼命地为这么多的内存创建备份存储。NET有点不寻常,因为它为线程提交堆栈空间,并提供执行保证。顺便说一句,app.exe.config文件中的
    元素应该具有非常明显的效果

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

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


    好吧,让你这么做而不摔倒是Windows的荣耀。但显然这是个坏主意。最多使用几个线程。或者,如果写入操作将使文件系统缓存饱和,则只需执行一次,这样就可以避免搜索惩罚。

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

    这一点需要特别注意,因为简单线程的工作时间越长,确保磁盘在计算过程中不处于空闲状态的好处就越大


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

  • 您正在创建90000个线程-这既昂贵又复杂 没必要
  • 你正在锁定所有的工作,使这个单线程!
  • 是的,没有锁你会得到一个例外。。。这并不能神奇地使锁从性能的角度成为一个好主意——它只是意味着你有错误的代码
  • 进入线程的一个简单快捷的方法是使用任务并行库,它的危险性稍微小一些(尽管您仍然可以填充它)。例如:

    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);
            }
        }
    }
    
    使用系统;
    使用系统诊断;
    使用System.IO;
    使用System.Threading.Tasks;
    命名空间控制台应用程序15
    {
    班级计划
    {
    const int FILE_COUNT=9000;
    常数int DATA_长度=100;
    静态void Main(字符串[]参数)
    {
    如果(Directory.Exists(@“c:\Temp\”)目录。删除(@“c:\Temp\”,true);
    CreateDirectory(@“c:\Temp\”);
    var watch=S
    
      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();
    
    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);
            }
        }
    }