【问题标题】:Speed Up with multithreading多线程加速
【发布时间】:2011-10-11 18:26:40
【问题描述】:

我的程序中有一个 parse 方法,它首先从磁盘读取文件,然后解析行并为每一行创建一个对象。对于每个文件,之后都会保存包含行中对象的集合。文件大约300MB。 这大约需要 2.5-3 分钟才能完成。

我的问题:如果我将任务拆分为一个线程,只从磁盘读取文件,另一个线程解析行,第三个线程保存集合,我能否期望显着加快速度?或者这可能会减慢这个过程?

现代笔记本硬盘读取 300MB 多长时间?我认为,瓶颈是我任务中的 CPU,因为如果我执行该方法,CPU 的一个核心始终处于 100%,而磁盘空闲时间超过一半。

你好,雨

编辑:

private CANMessage parseLine(String line)
    {
        try
        {
            CANMessage canMsg = new CANMessage();
            int offset = 0;
            int offset_add = 0;

            char[] delimiterChars = { ' ', '\t' };

            string[] elements = line.Split(delimiterChars);

            if (!isMessageLine(ref elements))
            {
                return canMsg = null;
            }

            offset = getPositionOfFirstWord(ref elements);

            canMsg.TimeStamp = Double.Parse(elements[offset]);

            offset += 3;

            offset_add = getOffsetForShortId(ref elements, ref offset);

            canMsg.ID = UInt16.Parse(elements[offset], System.Globalization.NumberStyles.HexNumber);
            offset += 17;   // for signs between identifier and data length number
            canMsg.DataLength = Convert.ToInt16(elements[offset + offset_add]);
            offset += 1;
            parseDataBytes(ref elements, ref offset, ref offset_add, ref canMsg);
            return canMsg;
        }
        catch (Exception exp)
        {
            MessageBox.Show(line);
            MessageBox.Show(exp.Message + "\n\n" + exp.StackTrace);
            return null;
        }   
    }
}

这就是解析方法。它以这种方式工作,但也许你是对的而且它效率低下。我有 .NET Framwork 4.0,我在 Windows 7 上。我有一个 Core i7,每个内核都有 HypterThreading,所以我只使用了大约 1/8 的 cpu。

EDIT2:我正在使用 Visual Studio 2010 Professional。此版本中似乎没有用于性能分析的工具(根据 msdn MSDN Beginners Guide to Performance Profiling)。

EDIT3:我现在更改了代码以使用线程。现在看起来像这样:

foreach (string str in checkedListBoxImport.CheckedItems)
{
    toImport.Add(str); 
}

for(int i = 0; i < toImport.Count; i++)
{
    String newString = new String(toImport.ElementAt(i).ToArray());
    Thread t = new Thread(() => importOperation(newString));
    t.Start();
}

虽然您在上面看到的解析是在 importOperation(...) 中调用的。

使用此代码可以将时间从大约 2.5 分钟减少到“仅”40 秒。我遇到了一些我必须跟踪的并发问题,但至少这比以前快得多。

感谢您的建议。

【问题讨论】:

    标签: multithreading disk


    【解决方案1】:

    您不太可能获得一致的笔记本电脑硬盘性能指标,因为我们不知道您的笔记本电脑的使用年限,也不知道它是处于销售状态还是在旋转。

    考虑到您已经完成了一些基本的分析,我敢打赌 CPU 确实是您的瓶颈,因为单线程应用程序不可能使用超过 100% 的单个 cpu。这当然忽略了您的操作系统将进程拆分为多个内核和其他奇怪的东西。如果您获得 5% 的 CPU 使用率,则很可能是 IO 瓶颈。

    也就是说,您最好的选择是为您正在处理的每个文件创建一个新的线程任务,并将其发送到池线程管理器。您的线程管理器应该将您正在运行的线程数限制为您可用的内核数或者如果内存是一个问题(您确实说过您毕竟生成了 300MB 文件)您可以用于该进程的最大内存量.

    最后,要回答您不想为每个操作使用单独线程的原因,请考虑一下您对性能瓶颈的了解。您在 cpu 处理而不是 IO 方面遇到瓶颈。这意味着如果您将应用程序拆分为单独的线程,您的读取和写入线程大部分时间都将处于饥饿状态,等待您的处理线程完成。此外,即使您让它们异步处理,您也存在内存不足的风险,因为您的读取线程继续消耗您的处理线程无法跟上的数据。

    因此,请注意不要立即启动每个线程,而是让它们由某种形式的阻塞队列管理。否则,当您在上下文切换上花费的时间多于处理时间时,您就会冒着使系统慢下来的风险。这当然是假设你没有先崩溃。

    【讨论】:

      【解决方案2】:

      目前尚不清楚您有多少这些 300MB 文件。在我的上网本上阅读一个 300MB 的文件大约需要 5 或 6 秒,通过快速测试。确实听起来您受 CPU 限制。

      线程可能会有所帮助,尽管它可能会使事情变得非常复杂。您还应该分析您当前的代码 - 很可能是您的解析效率低下。 (例如,如果您使用 C# 或 Java,并且在循环中连接字符串,这通常是一个性能“陷阱”,可以轻松补救。)

      如果您确实选择了多线程方法,那么为了避免磁盘抖动,您可能希望让一个线程将每个文件读入内存(一次一个),然后将其传递数据到解析线程池。当然,这假设您也有足够的内存来执行此操作。

      如果您可以指定平台并提供解析代码,我们或许可以帮助您优化它。目前我们真正能说的是,是的,听起来你受 CPU 限制。

      【讨论】:

        【解决方案3】:

        只有 300 MB 太长了。

        根据具体情况,还有不同的因素可能会影响性能,但通常情况下,读取硬盘仍然可能是最大的瓶颈,除非您在解析过程中发生了一些紧张的事情,这似乎是这里的情况,因为它从硬盘读取 300MB 只需要几秒钟(除非它的碎片可能很糟糕)。

        如果您在解析中有一些效率低下的算法,那么选择或提出更好的算法可能会更有益。如果您绝对需要该算法并且没有可用的算法改进,那么听起来您可能会被卡住。

        另外,不要尝试在多线程的同时进行多线程读写,你可能会减慢速度以增加查找。

        【讨论】:

          【解决方案4】:

          鉴于您认为这是一项 CPU 密集型任务,您应该会看到使用单独 IO 线程的吞吐量总体有所增加(否则您唯一的处理线程会在磁盘读/写操作期间阻塞等待 IO)。

          有趣的是,我最近遇到了类似的问题,并且通过运行单独的 IO 线程(以及足够的计算线程来加载所有 CPU 内核)确实看到了显着的净改进。

          您没有说明您的平台,但我为我的 .NET 解决方案使用了任务并行库和 BlockingCollection,并且实现几乎是微不足道的。 MSDN 提供了一个很好的例子。

          更新:

          正如 Jon 所说,与计算所花费的时间相比,花费在 IO 上的时间可能很小,因此虽然您可以期待改进,但最好的时间利用可能是分析和改进计算本身。使用多个线程进行计算会大大加快速度。

          【讨论】:

          • 如果这个任务是 CPU 密集型的,那么我认为代码肯定有改进的空间。 :)
          【解决方案5】:

          嗯.. 300MB 的行必须拆分成很多 CAN 消息对象 - 讨厌!我怀疑这个技巧可能是在避免读取和写入操作之间过度的磁盘抖动的同时线程化消息程序集。

          如果我将此作为“新鲜”的要求(当然,以我 20/20 的事后看来,知道 CPU 将成为问题),我可能会只使用一个线程来读取,一个用于写入磁盘,并且最初至少为消息对象程序集编写一个线程。使用多个线程进行消息组装意味着在处理后重新排序对象以防止输出文件被乱序写入的复杂性。

          我会定义一个很好的磁盘友好大小的块类行和消息对象数组实例,比如 1024 个,并在启动时创建一个块池,比如 16 个,然后将它们推送到存储队列中。这控制和限制内存使用,大大减少 new/dispose/malloc/free,(看起来你现在有很多这样的!),提高磁盘 r/w 操作的效率,因为只执行大 r/w ,(除了最后一个块,通常只会部分填充),提供固有的流控制,(读取线程不能“逃跑”,因为池将用完块并且读取线程将阻塞池,直到写入线程返回一些块),并禁止过多的上下文切换,因为只处理大块。

          读取线程打开文件,从队列中获取块,读取磁盘,解析成行并将行塞入块中。然后它将整个块排队到处理线程并循环以从池中获取另一个块。读取线程可能在启动时或空闲时在其自己的输入队列中等待包含读/写文件规范的消息类实例。写入文件规范可以通过块的字段传播,因此向写入线程提供所需的一切。块。这构成了一个很好的子系统,文件规范可以在其中排队,并且无需任何进一步干预即可处理所有文件。

          处理线程从其输入队列中获取块并将行拆分为块中的消息对象,然后将完成的整个块排队到写入线程。

          写入线程将消息对象写入输出文件,然后将块重新排入存储池队列以供读取线程重复使用。

          所有队列都应该阻塞生产者-消费者队列。

          线程子系统的一个问题是完成通知。当写线程写完文件的最后一块时,它可能需要做点什么。我可能会以最后一个块作为参数触发一个事件,以便事件处理程序知道哪个文件已被完全写入。我可能会与错误通知类似。

          如果这还不够快,你可以试试:

          1) 使用互斥锁确保在块磁盘化期间读取和写入线程不会被其他线程抢占。如果你的块足够大,这可能不会有太大的不同。

          2) 使用多个处理线程。如果你这样做,块可能会“乱序”到达写线程。您可能需要一个本地列表,并且可能需要块中的某种序列号,以确保磁盘写入正确排序。

          祝你好运,无论你想出什么设计......

          Rgds, 马丁

          【讨论】:

          • 为什么“讨厌”?我别无选择。我将文本文件作为另一个软件包的输出。有没有比我做的更好的方法来解析它,或者“讨厌”是什么意思?
          猜你喜欢
          • 2012-05-17
          • 1970-01-01
          • 1970-01-01
          • 2012-04-18
          • 1970-01-01
          • 1970-01-01
          • 2017-11-03
          • 1970-01-01
          • 2020-05-24
          相关资源
          最近更新 更多