如何在多线程应用程序中实现 100% 的 CPU 使用率?

Posted

技术标签:

【中文标题】如何在多线程应用程序中实现 100% 的 CPU 使用率?【英文标题】:How to achieve 100% CPU usage in multithreaded application? 【发布时间】:2017-11-03 14:09:14 【问题描述】:

我有大约 100 个文本文件,每个 200MB,我需要解析它们。下面的程序加载文件并并行处理它们。它可以为每个文件创建一个线程,也可以为每个文件创建一个进程。

问题:如果我使用线程,它永远不会使用 100% 的 CPU,并且需要更长的时间才能完成。

THREAD PER FILE
total time: 430 sec
CPU usage 15-20%
CPU frequency 1.2 GHz

PROCESS PER FILE
total time 100 sec
CPU usage 100%
CPU frequency 3.75 GHz

我正在使用 E5-1650 v3 Hexa-Core 和 HT,因此我一次处理 12 个文件。

如何实现线程 100% 的 CPU 利用率?

下面的代码不使用处理结果,因为它不影响问题。

using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;

namespace libsvm2tsv

    class Program
    
        static void Main(string[] args)
        
            var sw = Stopwatch.StartNew();

            switch (args[0])
            
                case "-t": LoadAll(args[1], LoadFile); break;
                case "-p": LoadAll(args[1], RunChild); break;
                case "-f": LoadFile(args[1]); return;
            

            Console.WriteLine("ELAPSED: 0 sec.", sw.ElapsedMilliseconds / 1000);
            Console.ReadLine();
        

        static void LoadAll(string folder, Action<string> algorithm)
        
            var sem = new SemaphoreSlim(12);
            Directory.EnumerateFiles(folder).ToList().ForEach(f=> 
                sem.Wait();
                new Thread(() =>  try  algorithm(f);  finally  sem.Release();  ).Start();
            );
        

        static void RunChild(string file)
        
            Process.Start(new ProcessStartInfo
            
                FileName = Assembly.GetEntryAssembly().Location,
                Arguments = "-f \"" + file + "\"",
                UseShellExecute = false,
                CreateNoWindow = true
            )
            .WaitForExit();
        

        static void LoadFile(string inFile)
        
            using (var ins = File.OpenText(inFile))
                while (ins.Peek() >= 0)
                    ParseLine(ins.ReadLine());
        

        static long[] ParseLine(string line)
        
            return line
                .Split()
                .Skip(1)
                .Select(r => (long)(double.Parse(r.Split(':')[1]) * 1000))
                .Select(r => r < 0 ? -1 : r)
                .ToArray();
        
    

【问题讨论】:

简化代码,解释实际问题,不要尝试自己处理线程。 TPL 旨在使此类事情变得更容易。也不要拆分字符串,Regex 的速度要快几个数量级 并且 不会生成临时字符串。它快得多,您可能不需要多线程处理。 吞吐量可能受到磁盘而不是 CPU 的限制。因此,CPU 正在等待且未完全加载。 最后,您的代码是 IO 绑定的,而不是 CPU 绑定的。使用异步方法避免在等待 IO 完成时阻塞线程 @harold 这是同一个问题。使用自旋锁达到 100% 的 CPU 利用率有什么意义?还是执行垃圾收集?缓存意味着在一般情况下,多个核心拥有要处理的数据。也许不是全部,也许一半会加载数据,另一半会处理它 最初的想法与@AxelKemper 相同。 I/O 性能通常是此类操作的瓶颈,因此请确保您的磁盘足够快,最好是 SSD。并且不要将线程数限制为内核数(* 2 用于超线程 CPU)。在处理过程中,有时 CPU 会等待数据/指令的内部传输。所以更多的线程意味着更多的 CPU 利用率。哦,当然通常 100% 的利用率并不是最有效的!有些人认为 > 80% 是不可接受的,因为 CPU 资源冲突 = 更长的处理时间 【参考方案1】:

终于,我找到了瓶颈。我正在使用 string.Split 从每一行数据中解析数字,所以我得到了数十亿个短字符串。这些字符串被放入堆中。由于所有线程共享单个堆内存分配是同步的。由于进程具有单独的堆 - 不会发生同步并且事情运行得很快。这就是问题的根源。因此,我使用 IndexOf 而不是 Split 重写了解析,并且线程开始比单独的进程执行得更好。正如我所期望的那样。

由于 .NET 没有默认工具来解析字符串中某个位置的实数,所以我使用了这个:https://codereview.stackexchange.com/questions/75791/optimize-custom-double-parse,稍作修改。

using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;

namespace libsvm2tsv

    class Program
    

        static void Main(string[] args)
        
            var sw = Stopwatch.StartNew();

            switch (args[0])
            
                case "-t": LoadAll(args[1], LoadFile); break;
                case "-p": LoadAll(args[1], RunChild); break;
                case "-f": LoadFile(args[1]); return;
            

            Console.WriteLine("ELAPSED: 0 sec.", sw.ElapsedMilliseconds / 1000);
            Console.ReadLine();
        

        static void LoadAll(string folder, Action<string> algorithm)
        
            Parallel.ForEach(
                Directory.EnumerateFiles(folder),
                new ParallelOptions  MaxDegreeOfParallelism = 12 ,
                f => algorithm(f));
        

        static void RunChild(string file)
        
            Process.Start(new ProcessStartInfo
            
                FileName = Assembly.GetEntryAssembly().Location,
                Arguments = "-f \"" + file + "\"",
                UseShellExecute = false,
                CreateNoWindow = true
            )
            .WaitForExit();
        

        static void LoadFile(string inFile)
        
            using (var ins = File.OpenText(inFile))
                while (ins.Peek() >= 0)
                    ParseLine(ins.ReadLine());
        

        static long[] ParseLine(string line)
        
            // first, count number of items
            var items = 1;
            for (var i = 0; i < line.Length; i++)
                if (line[i] == ' ') items++;

            //allocate memory and parse items
            var all = new long[items];
            var n = 0;
            var index = 0;
            while (index < line.Length)
            
                var next = line.IndexOf(' ', index);
                if (next < 0) next = line.Length;
                if (next > index)
                
                    var v = (long)(parseDouble(line, line.IndexOf(':', index) + 1, next - 1) * 1000);
                    if (v < 0) v = -1;
                    all[n++] = v;

                
                index = next + 1;
            

            return all;
        

        private readonly static double[] pow10Cache;
        static Program()
        
            pow10Cache = new double[309];

            double p = 1.0;
            for (int i = 0; i < 309; i++)
            
                pow10Cache[i] = p;
                p /= 10;
            
        

        static double parseDouble(string input, int from, int to)
        
            long inputLength = to - from + 1;
            long digitValue = long.MaxValue;
            long output1 = 0;
            long output2 = 0;
            long sign = 1;
            double multiBy = 0.0;
            int k;

            //integer part
            for (k = 0; k < inputLength; ++k)
            
                digitValue = input[k + from] - 48; // '0'

                if (digitValue >= 0 && digitValue <= 9)
                
                    output1 = digitValue + (output1 * 10);
                
                else if (k == 0 && digitValue == -3 /* '-' */)
                
                    sign = -1;
                
                else if (digitValue == -2 /* '.' */ || digitValue == -4 /* ',' */)
                
                    break;
                
                else
                
                    return double.NaN;
                
            

            //decimal part
            if (digitValue == -2 /* '.' */ || digitValue == -4 /* ',' */)
            
                multiBy = pow10Cache[inputLength - (++k)];

                for (; k < inputLength; ++k)
                
                    digitValue = input[k + from] - 48; // '0'

                    if (digitValue >= 0 && digitValue <= 9)
                    
                        output2 = digitValue + (output2 * 10);
                    
                    else
                    
                        return Double.NaN;
                    
                

                multiBy *= output2;
            

            return sign * (output1 + multiBy);
        
    

【讨论】:

另一个修复是可能的。只需将 添加到 app.config。它使用许多内存块来避免锁定(类似于 ThreadStatic)。但是,与没有 string.Split() 的解决方案相比,内存分配开销约为 50-60%【参考方案2】:

我有大约 100 个文本文件,每个 200MB,我需要解析它们。

从/向旋转磁盘读取或写入数据的最快方法是顺序读取,以最大限度地减少磁盘磁头寻找数据或将数据写入指定位置所需的时间。因此,对单个磁盘执行并行 IO 会降低 IO 速率 - 并且根据实际的 IO 模式,它可以显着降低速率。可以按顺序处理 100 MB/秒的磁盘可能只能以每秒 20 或 30 千字节的速度并行读取/写入小块数据。

如果我优化这样的过程,我不会首先担心 CPU 利用率,我会先优化 IO 吞吐量。除非您正在执行一些真正的 CPU 密集型解析,否则您会受到 IO 限制。一旦您的 IO 吞吐量得到优化,如果您获得 100% 的 CPU 利用率,那么您将受到 CPU 限制。如果您的设计可以很好地扩展,那么您可以添加 CPU 并可能运行得更快。

要加快 IO,您首先需要尽量减少磁盘寻道,尤其是在您使用消费级廉价 SATA 驱动器的情况下。有多种方法可以做到这一点。

首先,最简单的 - 消除磁盘磁头。将您的数据放在 SSD 上。无需编写复杂、容易出错的优化代码即可解决问题。您需要多长时间才能使用软件加快运行速度?你必须设计一些东西,测试它,调整它,调试它,重要的是,保持它运行良好。这些都不是免费的。一项重要的成本是花时间让事情变得更快的机会成本——当你这样做时,你并没有解决任何其他问题。更快的硬件没有这些成本。在这种情况下,购买 SSD 并将其插入,速度会更快。

但如果您真的想花几周或更长时间来优化您的处理软件,我会这样做:

    将数据分布在多个磁盘上。您不能快速对物理磁盘执行并行 IO,因为磁盘磁头寻道会降低性能。因此,尽可能多地对不同磁盘进行读写操作。 对于每个磁盘,有一个读取器或写入器线程或进程将数据馈送到工作池或写入该工作池提供的数据。 执行实际解析的工作线程/进程的数量可调。

这样,您可以按顺序读取文件和写入输出数据,而不会与其他 IO 进程争用每个磁盘。

【讨论】:

是的。进行多步骤过程可能是有意义的。第一步读取文件 - 单线程 - 然后将内容交给多线程解析。【参考方案3】:

我会考虑将 ForEach 替换为 Parallel.ForEach 并删除您对线程的显式使用。使用https://***.com/a/5512363/34092 设置线程数限制。

static void LoadAll(string folder, Action<string> algorithm)

    Parallel.ForEach(Directory.EnumerateFiles(folder), algorithm);

【讨论】:

这对所有字符串拆分都没有帮助。也没有理由设置限制,Parallel.ForEach 这样做 我在本地测试了代码并达到了 99% 的 CPU 使用率(现有代码为 30-40%)。诚然,我是用一些测试文件来做的,所以也许这不是 Anton 所看到的(更重要的是,它实际上是否比基于过程的方法更快)。 用 Parallel 重写 - 同样的问题 如果您可以尝试使用我的真实文件 - 很高兴知道您的结果 基于过程的方法总是不好的。设置进程——尤其是 .NET——并不是非常快。提高效率一定是值得的。【参考方案4】:

正如其他人所说,IO 最终可能会成为瓶颈,获得 100% 的 CPU 使用率真的无关紧要。不过,我觉得他们遗漏了一些东西:与线程相比,进程的吞吐量确实更高,这意味着 IO 不是唯一的瓶颈。原因是 CPU 以更高的频率运行进程,并且您希望它在不等待 IO 时以峰值速度运行!那么,你该怎么做呢?

最简单的方法是从电源选项手动设置电源配置文件。编辑电源选项并将最小和最大处理器状态设置为 100%。这应该可以完成这项工作。

如果您想从您的程序中执行此操作,请查看How to Disable Dynamic Frequency Scaling?。 .NET 可能有类似的 API,但不使用本机代码,但我现在找不到。

【讨论】:

以上是关于如何在多线程应用程序中实现 100% 的 CPU 使用率?的主要内容,如果未能解决你的问题,请参考以下文章

记一次线上故障--HashMap在多线程条件下运行造成CPU 100%

记一次线上故障--HashMap在多线程条件下运行造成CPU 100%

HashMap为什么在多线程下会让cpu100%

核心数据:在多线程 iOS 应用程序中实现多个 NSManagedObjects 和 NSFetchedResultsController 非常困惑

当 CPU 负载为 100%(主要使用 C++ 和 Qt)时,如何保持 UI 响应?

原来,JDK8的ConcurrentHashMap也会造成CPU 100%