我有一个自定义线程池类,它创建一些线程,每个线程等待自己的事件(信号)。将新作业添加到线程池时,它会唤醒第一个空闲线程,以便它执行作业。
问题如下:我有大约1000个循环,每个循环大约10'000次迭代。这些循环必须按顺序执行,但我有4个CPU可用。我尝试做的是将10'000次迭代循环分成4个2'500次迭代循环,即每个线程一次。但是我必须等待4个小循环才能完成下一个“大”迭代。这意味着我无法捆绑这些工作。
我的问题是使用线程池和4个线程比顺序执行作业要慢得多(由一个单独的线程执行一个循环比直接在主线程中顺序执行它要慢得多。)
我在Windows上,因此我使用CreateEvent()
创建事件,然后使用WaitForMultipleObjects(2, handles, false, INFINITE)
等待其中一个事件,直到主线程调用SetEvent()
。
看来整个事件(以及使用关键部分的线程之间的同步)非常昂贵!
我的问题是:使用事件花费“很多”时间是否正常?如果是这样,我可以使用另一种机制,而且时间更便宜吗?
这里有一些代码来说明(从我的线程池类复制的一些相关部分):
// thread function
unsigned __stdcall ThreadPool::threadFunction(void* params) {
// some housekeeping
HANDLE signals[2];
signals[0] = waitSignal;
signals[1] = endSignal;
do {
// wait for one of the signals
waitResult = WaitForMultipleObjects(2, signals, false, INFINITE);
// try to get the next job parameters;
if (tp->getNextJob(threadId, data)) {
// execute job
void* output = jobFunc(data.params);
// tell thread pool that we're done and collect output
tp->collectOutput(data.ID, output);
}
tp->threadDone(threadId);
}
while (waitResult - WAIT_OBJECT_0 == 0);
// if we reach this point, endSignal was sent, so we are done !
return 0;
}
// create all threads
for (int i = 0; i < nbThreads; ++i) {
threadData data;
unsigned int threadId = 0;
char eventName[20];
sprintf_s(eventName, 20, "WaitSignal_%d", i);
data.handle = (HANDLE) _beginthreadex(NULL, 0, ThreadPool::threadFunction,
this, CREATE_SUSPENDED, &threadId);
data.threadId = threadId;
data.busy = false;
data.waitSignal = CreateEvent(NULL, true, false, eventName);
this->threads[threadId] = data;
// start thread
ResumeThread(data.handle);
}
// add job
void ThreadPool::addJob(int jobId, void* params) {
// housekeeping
EnterCriticalSection(&(this->mutex));
// first, insert parameters in the list
this->jobs.push_back(job);
// then, find the first free thread and wake it
for (it = this->threads.begin(); it != this->threads.end(); ++it) {
thread = (threadData) it->second;
if (!thread.busy) {
this->threads[thread.threadId].busy = true;
++(this->nbActiveThreads);
// wake thread such that it gets the next params and runs them
SetEvent(thread.waitSignal);
break;
}
}
LeaveCriticalSection(&(this->mutex));
}
答案 0 :(得分:3)
是的,WaitForMultipleObjects
非常昂贵。如果您的工作量很小,那么同步开销将开始压倒实际执行工作的成本,正如您所看到的那样。
解决这个问题的一种方法是将多个作业捆绑成一个:如果你得到一个“小”工作(不管你评价这些东西),把它存放到某个地方,直到你有足够的小工作来组成一个合理大小的工作。然后将它们全部发送到工作线程进行处理。
或者,您可以使用多读取器单写入器队列来存储您的作业,而不是使用信令。在此模型中,每个工作线程都尝试从队列中获取作业。当它找到一个,它就完成了工作;如果没有,它会睡一小段时间,然后醒来再试一次。这将降低每个任务的开销,但即使没有工作要做,你的线程也会占用CPU。这一切都取决于问题的确切性质。
答案 1 :(得分:3)
这在我看来是一个生产者消费者模式,它可以用两个信号量实现,一个保护队列溢出,另一个保护空队列。
您可以找到一些详细信息here。
答案 2 :(得分:2)
注意,在发出endSignal之后,你仍然要求下一份工作。
for( ;; ) {
// wait for one of the signals
waitResult = WaitForMultipleObjects(2, signals, false, INFINITE);
if( waitResult - WAIT_OBJECT_0 != 0 )
return;
//....
}
答案 3 :(得分:1)
线程之间的上下文切换也很昂贵。在某些情况下,开发一个可用于通过一个线程或多个线程顺序处理作业的框架很有意思。这样你就可以拥有两个世界中最好的一个。
顺便问一下,你的问题到底是什么?我将能够用更精确的问题更精确地回答:)
编辑:
在某些情况下,事件部分可能比处理消耗更多,但不应该那么昂贵,除非你的处理真的很快。在这种情况下,在thredas之间切换也很昂贵,因此我的第一部分回答顺序做事......
您应该查找线程间同步瓶颈。您可以跟踪线程等待时间开始......
编辑:经过更多提示......
如果我猜对了,那么你的问题就是要有效地使用你所有的计算机内核/处理器,以便对一些处理过程进行精简序列化。
假设您有4个内核和10000个循环来计算,如示例中所示(在注释中)。你说你需要等待4个线程结束才能继续。然后,您可以简化同步过程。你只需要给你的四个线程thth,nth + 1,nth + 2,nth + 3个循环,等待四个线程完成然后继续。您应该使用集合点或屏障(等待n个线程完成的同步机制)。 Boost有这样一种机制。您可以查看Windows实现以提高效率。您的线程池不适合该任务。在关键部分中搜索可用线程会占用您的CPU时间。不是活动的一部分。
答案 4 :(得分:1)
它不应该那么昂贵,但如果你的工作几乎不需要任何时间,那么线程和同步对象的开销就会变得很大。像这样的线程池对于长处理作业或者那些使用大量IO而不是CPU资源的作业来说效果更好。如果在处理作业时遇到CPU限制,请确保每个CPU只有1个线程。
可能还有其他问题,getNextJob如何处理其数据?如果有大量数据复制,那么您再次显着增加了开销。
我会通过让每个线程继续从队列中拉出作业来优化它,直到队列为空。这样,您可以将一百个作业传递给线程池,同步对象将只使用一次来启动线程。我还将作业存储在队列中,并将指针,引用或迭代器传递给线程而不是复制数据。
答案 5 :(得分:1)
看来这整个事件的事情 (以及同步 使用critical之间的线程之间 部分)非常昂贵!
“昂贵”是一个相对术语。喷气机价格昂贵吗?是汽车吗?还是自行车......鞋......?
在这种情况下,问题是:相对于JobFunction执行所花费的时间,事件是“昂贵的”吗?这将有助于公布一些绝对数字:“无螺纹”时该过程需要多长时间?是几个月,还是几个飞秒?
增加线程池大小时会发生什么?尝试池大小为1,然后是2,然后是4,等等。
另外,由于过去你在线程池中遇到过一些问题,我建议进行一些调试 计算实际调用线程函数的次数...它是否符合您的预期?
从空中挑选一个数字(不知道你的目标系统的任何信息,并假设你没有在你没有显示的代码中做任何'巨大的'),我期望每个人的“事件开销” “工作”以微秒为单位。也许一百左右。如果在JobFunction中执行算法所花费的时间并不比这次大得多,那么你的线程可能会花费你的时间而不是保存它。
答案 6 :(得分:1)
如果您只是并行化循环并使用vs 2008,我建议您查看OpenMP。如果您正在使用visual studio 2010 beta 1,我建议您查看parallel pattern library,特别是"parallel for" / "parallel for each" apis或"task group类,因为这些可能会执行您尝试的内容做,只用较少的代码。
关于你的性能问题,这取决于它。您需要查看迭代期间安排的工作量以及成本。 WaitForMultipleObjects可能非常昂贵,如果你经常使用它并且你的工作很小,这就是为什么我建议使用已经构建的实现。您还需要确保您没有在调试模式下运行调试模式,并且任务本身在锁,I / O或内存分配上没有阻塞,并且您没有遇到错误共享。其中每一项都有可能破坏可扩展性。
我建议在视觉工作室2010测试版1中的xperf f1剖析器(它有2种新的并发模式,有助于查看争用)或英特尔的vtune下查看此内容。
你也可以分享你在任务中运行的代码,这样人们可以更好地了解你正在做什么,因为我总是得到性能问题的答案首先是“它取决于”,其次, “你有没有把它描出来。”
祝你好运
-Rick
答案 7 :(得分:1)
由于你说它并行多比顺序执行慢,我假设你的内部2500循环迭代的处理时间很短(在几微秒范围内)。然后除了检查你的算法以分割更大的进动块之外,你无能为力; OpenMP无济于事,其他所有同步技术都无济于事,因为它们基本上都依赖于事件(自旋循环不符合条件)。
另一方面,如果2500次循环迭代的处理时间大于100微秒(在当前PC上),则可能会遇到硬件限制。如果您的处理使用大量内存带宽,将其拆分为四个处理器将无法为您提供更多带宽,实际上它会因冲突而减少。您还可能遇到缓存循环问题,其中您的前1000次迭代中的每一个都将刷新并重新加载4个核心的缓存。然后没有一个解决方案,并且根据您的目标硬件,可能没有。
答案 8 :(得分:0)
如前所述,线程添加的开销量取决于执行您定义的“作业”所花费的相对时间。因此,重要的是找到工作块大小的平衡,以最小化块数,但不会让处理器空闲等待最后一组计算完成。
您的编码方法通过积极寻找空闲线程来提供新工作,从而增加了开销工作量。操作系统已经跟踪并更有效地完成了这项工作。此外,您的函数ThreadPool :: addJob()可能会发现所有线程都在使用中,并且无法委派工作。但它不提供与该问题相关的任何返回代码。如果您没有以某种方式检查此情况并且没有注意到结果中的错误,则表示始终存在空闲处理器。我建议重新组织代码,以便addJob()执行它的命名 - 仅添加一个作业(找不到甚至关心谁做这个工作),而每个工作线程在完成现有工作时主动获得新工作。