通过多线程加速

时间:2011-10-11 06:17:02

标签: multithreading disk

我的程序中有一个解析方法,它先从磁盘读取一个文件,然后解析这些行并为每一行创建一个对象。对于每个文件,随后保存具有行中对象的集合。这些文件大约是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秒。我得到了一些我必须跟踪的并发问题,但至少比以前快得多。

感谢您的建议。

5 个答案:

答案 0 :(得分:0)

目前还不清楚这些300MB文件中有多少是你得到的。单个300MB文件需要大约5或6秒才能在我的上网本上阅读,并进行快速测试。它确实听起来像你受CPU限制。

线程可能会有所帮助,尽管它当然可能会使事情变得复杂。您还应该对当前代码进行概要分析 - 很可能是您只是无效地解析。 (例如,如果您正在使用C#或Java并且您在循环中连接字符串,那么这通常是一个可以轻松解决的性能“问题”。)

如果您 选择多线程方法,那么为了避免颠簸磁盘,您可能希望让一个线程将每个文件读入内存(一次一个)然后传递数据到解析线程池。当然,这假设你也有足够的记忆力。

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

答案 1 :(得分:0)

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

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

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

更新:

正如Jon指出的那样,与计算所花费的时间相比,花在IO上的时间可能很少,所以虽然你可以期待一个改进,但最好的时间利用可能是分析和改进计算本身。使用多个线程进行计算会显着加快。

答案 2 :(得分:0)

只有300 MB的时间很长。

根据具体情况,有不同的因素可能影响性能,但通常情况下读取硬盘仍然可能是最大的瓶颈,除非你在解析过程中遇到激烈的事情,这似乎是因为它只需要几秒钟就可以从硬盘中读取300MB(除非它的碎片可能很碎)。

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

此外,不要尝试使用多线程同时读取和写入多线程,您可能会放慢速度以增加搜索。

答案 3 :(得分:0)

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

考虑到你已经做了一些基本的分析,我认为CPU确实是你的瓶颈,因为单线程应用程序不可能使用100%以上的单个cpu。这当然忽略了您的操作系统将进程分成多个核心和其他奇怪的东西。如果你的CPU使用率提高了5%,那很可能是IO瓶颈缩。

那说你最好的选择是为你正在处理的每个文件创建一个新的线程任务,并将其发送给池化线程管理器。您的线程管理器应该将您运行的线程数量限制为您可用的核心数量,或者内存是否存在问题(您确实说您生成了300MB文件)您可以使用的最大ram数量

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

因此,请注意不要立即启动每个线程,而是让它们由某种形式的阻塞队列管理。否则,您可能会因为在上下文切换中花费更多时间而不是处理而将系统速度降低到爬行的风险。这当然是假设你没先崩溃。

答案 4 :(得分:0)

嗯.. 300MB的行必须分成很多CAN消息对象 - 讨厌!我怀疑诀窍可能是线程化消息组件,同时避免读写操作之间过多的磁盘抖动。

如果我这样做是一个'新鲜'的要求,(当然,我的20/20后见之明,知道CPU将成为问题),我可能只使用一个线程进行阅读,一个用于写入磁盘,并且最初至少为消息对象程序集编写一个线程。使用多个线程进行消息组装意味着在处理之后重新排序对象的复杂性,以防止输出文件被无序写入。

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

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

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

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

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

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

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

1)确保通过使用互斥锁在读取块的过程中,不能使读取和写入线程有效地支持另一个。如果你的大块足够大,这可能不会产生太大的影响。

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

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

RGDS, 马丁