使用事件导致的开销

8
我有一个自定义的线程池类,它创建了一些线程,每个线程都等待其自己的事件(信号)。当新的作业被添加到线程池时,它唤醒第一个空闲线程以便执行该作业。
问题如下:我有大约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));
}

编辑以精确你的问题。 - neuro
9个回答

3
是的,WaitForMultipleObjects非常昂贵。如果您的任务很小,则同步开销将开始超过实际执行任务的成本,正如您所看到的。
解决此问题的一种方法是将多个作业捆绑在一起:如果您获得“小”作业(无论您如何评估此类事物),请将其存储在某个地方,直到有足够的小作业组合成一个合理大小的作业。然后将它们全部发送到工作线程进行处理。
另外,您可以使用多读单写队列而不是使用信号来存储作业。在这种模型中,每个工作线程都尝试从队列中获取作业。当它找到一个时,它会执行该作业;如果没有找到,则会短暂睡眠,然后醒来并再次尝试。这将降低您的每项任务开销,但即使没有工作要做,您的线程也会占用CPU。这完全取决于问题的确切性质。

问题如下:我有大约1000个循环,每个循环大约有10,000次迭代需要执行。这些循环必须按顺序执行,但我有4个可用的CPU。我尝试的做法是将10,000次迭代循环分成4个2,500次迭代循环,即每个线程一个循环。但在进行下一个“大”迭代之前,我必须等待4个小循环完成。这意味着我不能捆绑作业。 - Wookai

3
这对我来说看起来像是生产者消费者模式,可以用两个信号量实现,一个保护队列溢出,另一个保护空队列。
你可以在这里找到一些细节。

信号量比事件更便宜吗? - Wookai
“expensive”是什么意思?是指资源方面的开销吗?还是指内核花费的时间来锁定/解锁? - Cătălin Pitiș
我认为没有什么区别。无论如何,这是可以看到的差异。您始终可以使用分析器进行测量。 - Cătălin Pitiș
好的,谢谢。感谢您提供链接,很有趣。然而,我不确定使用生产者/消费者模式来实现它能加速处理速度。 - Wookai

2

注意,当endSignal被触发后,你仍然在请求下一个作业。

for( ;; ) {
    // wait for one of the signals
    waitResult = WaitForMultipleObjects(2, signals, false, INFINITE);
    if( waitResult - WAIT_OBJECT_0 != 0 )
        return;
    //....
}

谢谢指出。这不是问题,因为当作业列表为空时,endSignal被调用,因此它不会获取任何作业并将正确结束。但你说得完全正确! - Wookai

2

你说并行执行比顺序执行慢得多,我猜测你内部的2500次循环处理时间非常短(在几微秒范围内)。这种情况下,除了检查算法以拆分更大的处理块之外,OpenMP和其他同步技术都无法提供帮助,因为它们基本上都依赖于事件(自旋循环不符合条件)。

另一方面,如果你的2500次循环处理时间大于100微秒(在当前PC上),那么你可能会遇到硬件限制。如果你的处理使用了大量的内存带宽,将其分成四个处理器不会给你更多的带宽,实际上会因为冲突而使你的带宽更少。你还可能遇到缓存循环问题,其中每个前1000次迭代会刷新和重新加载4个核的缓存。这时就没有一个解决方案,取决于你的目标硬件,可能根本没有解决方案。


谢谢您的见解!OpenMP确实有所帮助,但它主要是通过允许我摆脱自定义线程池并依赖于更可靠的东西来帮助我的。 - Wookai
OpenMP 可能有所帮助,因为它使用当前线程进行执行。因此,在您的情况下,同步减少了 20%。此外,它通常在睡眠之前使用小的自旋循环实现,因此,如果您的执行速度很快,它可能有助于在许多情况下完全消除事件。 - Juice

1

如前所述,线程添加的开销取决于您定义的“作业”所需的时间相对量。因此,重要的是要找到一种平衡,使工作块的大小最小化,但不会让处理器空闲等待最后一组计算完成。

您的编码方法通过积极寻找空闲线程来提高了开销。操作系统已经在跟踪并更加高效地执行这项任务。此外,您的函数ThreadPool::addJob()可能会发现所有线程都在使用中,无法委派工作。但它没有提供与该问题相关的返回代码。如果您没有以某种方式检查此条件并且没有注意到结果中的错误,则意味着始终存在空闲处理器。我建议重新组织代码,使addJob()只执行其命名的操作--仅添加作业(而不查找或关心谁执行作业),而每个工作线程在完成现有工作后积极获取新工作。


1

线程之间的上下文切换也可能很昂贵。在某些情况下,开发一个框架以便您可以使用单个线程或多个线程顺序处理作业是有趣的。这样,您就可以拥有两个世界中最好的。

顺便问一下,您的问题确切是什么?我将能够通过更精确的问题回答更精确的问题:)

编辑:

事件部分在某些情况下可能会消耗比处理更多的内容,但除非您的处理速度真的很快,否则不应该那么昂贵。在这种情况下,线程之间的切换也很昂贵,因此我的答案首先是按顺序执行任务...

您应该寻找线程间同步瓶颈。首先可以跟踪线程等待时间...

编辑:经过更多提示...

如果我猜得正确,您的问题是有效地使用所有计算机核心/处理器来并行化某些本质上是顺序的处理。

假设你有4个核心和10000个循环需要计算,就像你在评论中的例子一样。你说你需要等待4个线程结束才能继续。那么你可以简化同步过程。你只需要给你的四个线程第n、n+1、n+2、n+3个循环,等待四个线程完成后再继续。你应该使用一个会合点或屏障(一种等待n个线程完成的同步机制)。Boost有这样的机制。你可以查看Windows实现以提高效率。你的线程池并不适合这个任务。在关键部分搜索可用线程是导致CPU时间浪费的原因,而不是事件部分。


嗯,我想我的问题是关于使用事件的成本(它们真的很昂贵还是我做错了什么?)。 - Wookai
1
neuro的方法可能是你最好的选择。如果可以的话,你的另一个选择是重新设计循环,使它们不再相互依赖。你可能需要承担一些性能损失,但没关系:代码速度慢2倍,但随着硬件线程数量的增加呈线性扩展的代码总体上胜出,对吧? - David Seiler

1

这不应该太昂贵,但如果你的工作几乎没有花费任何时间,那么线程和同步对象的开销将变得非常重要。像这样的线程池更适合长时间处理的工作或使用大量IO而不是CPU资源的工作。如果在处理作业时受到CPU限制,请确保每个CPU只有1个线程。

可能还有其他问题,getNextJob如何获取其要处理的数据?如果存在大量数据复制,则再次增加了开销。

我会通过让每个线程不断从队列中取出作业直到队列为空来进行优化。这样,您可以向线程池传递一百个作业,并且同步对象将仅用于一次以启动线程。我还会将作业存储在队列中,并将指针、引用或迭代器传递给线程,而不是复制数据。


我和你一样有同样的优化想法,即让线程在不经过WaitForMultipleObjects()的情况下拉取作业,但在我的情况下,每个线程只有很少的作业,所以这不会改变太多。 - Wookai
我以为每个线程你有2500?不要紧 - 另一种选择是尝试使用OpenMP,这可能会更快,并且绝对更容易实现。(也就是说,在for循环之前放置#pragma,让它为您管理一切)。 - gbjbaanb

1
似乎整个事件(以及使用关键部分区别同步线程之间)是非常昂贵的!“昂贵”是一个相对的词。 喷气式飞机贵吗?汽车?或自行车...鞋子...?在这种情况下,问题是:相对于执行JobFunction所需的时间,事件是否“昂贵”? 发布一些绝对数字会有所帮助:当“未线程化”时,该过程需要多长时间? 是几个月还是几飞秒?随着您增加线程池大小,时间会发生什么变化? 尝试1、2、4等池大小。此外,由于您以前在此处使用线程池出现了一些问题,因此建议进行一些调试以计算实际调用线程函数的次数...它是否符合您的期望?
从空中挑选一个数字(不知道目标系统的任何信息,并假设您没有展示任何“巨大”的代码),我预计每个“作业”的“事件开销”应该以微秒为单位进行测量。可能是一百左右。如果在JobFunction中执行算法所花费的时间不明显超过这个时间,那么您的线程很可能会浪费时间而不是节省时间。

1

如果你只是并行化循环并使用vs 2008,我建议看看OpenMP。如果你正在使用visual studio 2010 beta 1,我建议看看parallel pattern library,特别是"parallel for" / "parallel for each" apis"task group类,因为这些可能会做你试图做的事情,只需要更少的代码。

关于你关于性能的问题,这取决于具体情况。你需要查看在迭代期间调度了多少工作以及成本如何。如果你经常遇到WaitForMultipleObjects,而且你的工作量很小,那么它可能会非常昂贵,这就是为什么我建议使用已经构建好的实现。你还需要确保你没有在调试模式下运行,在调试器下运行,任务本身没有在锁定、I/O或内存分配上阻塞,并且你没有遇到虚假共享。每个问题都有可能破坏可扩展性。

我建议您在像xperf这样的分析器下查看,或者使用Visual Studio 2010 Beta 1中的f1分析器(它有两种新的并发模式,可以帮助查看争用情况),或者使用英特尔的VTune。

您还可以分享您在任务中运行的代码,这样大家就可以更好地了解您在做什么,因为我在处理性能问题时总是得到的答案是“这取决于具体情况”,其次是“您是否对其进行了分析”。

祝您好运

-Rick


谢谢你的回答。我会接受你的建议,因为你提供了有用的链接并建议使用OpenMP! - Wookai

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