使用QThreads时CPU核心未能正确利用

7

使用:C++ (MinGW), Qt4.7.4, Vista (操作系统), Intel Core2vPro

我需要以完全相同的方式处理两个巨大的文件。因此,我想从2个单独的线程中调用处理例程来处理这两个不同的文件。GUI线程不会执行任何重任务;只是显示一个标签并运行事件循环以检查线程终止条件的发出,并相应地退出主应用程序。我希望它会充分利用两个内核(Intel Core2),但恰恰相反,从任务管理器中可以看到一个内核被高度利用而另一个内核没有被利用(虽然并非每次运行代码时都是如此);而且处理这两个文件所需的时间要比处理一个文件的时间长得多(我原以为应该是相等或略多一些,但这几乎相当于在非线程应用程序中依次处理这两个文件)。我能否通过某种方式强制线程使用我指定的内核?

QThread* ptrThread1=new QThread;
QThread* ptrThread2=new QThread;
ProcessTimeConsuming* ptrPTC1=new ProcessTimeConsuming();
ProcessTimeConsuming* ptrPTC2=new ProcessTimeConsuming();

ptrPTC1->moveToThread(ptrThread1);
ptrPTC2->moveToThread(ptrThread2);

//make connections to specify what to do when processing ends, threads terminate etc
//display some label to give an idea that the code is in execution

ptrThread1->start();
ptrThread2->start(); //i want this thread to be executed in the core other than the one used above

ptrQApplication->exec(); //GUI event loop for label display and signal-slot monitoring

7
这些文件是存储在不同的物理硬盘上吗?如果你试图同时读取两个磁盘文件,那么每当一个不同的线程被调度时,你必须在它们之间进行寻址,而这部分操作会使得CPU所获得的任何优势都被淹没。 - Pete Kirkham
这些文件大小大致相等吗? - Tudor
@PeteKirkham:只有1个硬盘。 - ustulation
3个回答

17

从单个机械硬盘并行读取通常不会(也许在你的情况下也是如此)产生任何性能增益,因为硬盘的机械头需要每次旋转以寻找下一个读取位置,有效地使您的读取成为顺序读取。更糟糕的是,如果有很多线程尝试读取,则性能甚至可能比顺序版本降低,因为磁盘头被弹到磁盘的不同位置,因此每次都需要回到上次离开的位置。

一般来说,你不能做得比按顺序读取文件然后使用生产者-消费者模型在并行处理它们时更好。


谢谢。。。我猜我需要坚持使用顺序处理,因为文件大小会阻止将其读入内存后进行处理。另外,我从未使用过boost threads..你能否给我一些提示,它是否具有此功能(核心亲和力API),以便我在需要时深入了解? - ustulation
@phresnel:哇...有趣的阅读..但我在速度上并没有真正体验到这一点(不知道我的操作系统是否实际执行了这个功能) - ustulation
@Tudor:你并没有完全错,而我也不完全正确(尽管我写了“不能确定”)。想象一下你有两个巨大的文件A和B,它们分散在硬盘上。进程1读取A,进程2读取B。现在,进程1读取了一个A块,该块与即将被进程2读取的B块相邻。下一个A块则很远。单线程严格意义上意味着进程1现在必须等待几秒钟,直到磁盘准备好下一个块。在这之后,进程2可以读取B,并面临同样的磁盘等待... - Sebastian Mach
@phresnel:谢谢。你提出这个观点很好。考虑多种可能性总是值得的。 - Tudor
当然,如果文件是有缺陷的,你可以“做得更好”,即在使用比顺序读取更少的内存方面。你可以并且应该明确地控制在文件之间寻找的开销 - 实现自己的轮询读取器。这不是魔术,物理限制相当简单。 - Kuba hasn't forgotten Monica
显示剩余15条评论

2
使用机械硬盘时,您需要明确控制顺序读取和寻道时间的比率。通常的方法是使用m+min(n, QThread::idealThreadCount())线程上运行的n+m个对象。其中,m是文件所在的硬盘数量,n是文件数量。
  • 每个m个对象以轮询方式从给定的硬盘读取文件。每次读取必须足够大。在现代硬盘上,假设预算为70兆字节/秒的带宽(可以基准测试实际值),5毫秒为一次寻道。为了浪费的带宽最多不超过10%,您只有100毫秒或100毫秒/(5ms/seek)=20次寻道/秒。因此,在从下一个文件读取之前,您必须至少从每个文件中读取70MB/(20次寻道+1)=3.3 MB。该线程使用文件数据填充缓冲区,然后缓冲区向与其连接的计算对象发出信号。当缓冲区繁忙时,您只需跳过读取给定文件,直到缓冲区再次可用。

  • 其他n个对象是计算对象,它们在缓冲区发出已满信号时执行计算。一旦不再需要缓冲区数据,缓冲区就会“重置”,以便文件读取器可以重新填充它。

所有阅读器对象都需要自己的线程。计算对象可以以轮询方式分布在它们自己的线程中,以便线程之间的对象数相差不超过1个。

1
我认为我的经验数据可能对这个讨论有所帮助。我有一个包含980个txt文件的目录,希望能够阅读。在Qt/C++框架上,运行于Intel i5四核处理器上,我创建了一个GUI应用程序,并添加了一个worker类来读取给定路径下的文件。我将worker推入一个线程中,然后每次运行都添加一个额外的线程。使用1个线程大约需要13分钟,使用2个线程需要9分钟,使用3个线程需要8分钟。因此,在我的情况下确实有一些好处,但效果很快就会降低。

任何允许单个线程耗尽其读/写容量的系统都会不稳定,或者至少对用户不响应。除非你特意去做,否则线程的输入/输出将被有效地限制。你所做的是通过启动两个线程要求你的程序具有更高的优先级。 - Mikhail
一切都取决于文件的大小。通常来说,在机械硬盘上,如果你希望开销在10%以下,你必须每次读取几兆字节。因此,如果文件小于2M字节,你可以完整地读取它们。如果它们更大,那么你可以在文件之间轮流进行,以保持更多的计算线程繁忙。 - Kuba hasn't forgotten Monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接