【问题标题】:Overhead due to use of Events由于使用事件而产生的开销
【发布时间】:2010-11-21 07:46:50
【问题描述】:

我有一个自定义线程池类,它创建了一些线程,每个线程都等待自己的事件(信号)。当一个新作业添加到线程池中时,它会唤醒第一个空闲线程以便执行该作业。

问题如下:我有大约 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));
}

【问题讨论】:

  • 编辑以明确您的问题...

标签: c++ multithreading synchronization events overhead


【解决方案1】:

这在我看来是一种生产者消费者模式,可以用两个信号量来实现,一个保护队列溢出,另一个保护空队列。

你可以找到一些细节here

【讨论】:

  • 信号量比事件便宜吗?
  • “贵”是什么意思?在资源方面?就锁定/解锁所花费的内核时间而言?
  • 我认为没有区别。无论如何,可以看到的差异。您始终可以使用分析器进行测量。
  • 好的,谢谢。谢谢你的链接,这很有趣。但是,我不确定使用生产者/消费者模式来实现它会加快速度。
【解决方案2】:

是的,WaitForMultipleObjects 相当昂贵。如您所见,如果您的工作量很小,同步开销将开始超过实际执行工作的成本。

解决此问题的一种方法是将多个作业捆绑为一个:如果您获得了一份“小”作业(无论您如何评估此类事情),请将其存储在某个地方,直到您有足够的小作业一起完成一个合理大小的作业。然后将它们全部发送到工作线程进行处理。

或者,您可以使用多读取器单写入器队列来存储您的作业,而不是使用信号。在此模型中,每个工作线程都尝试从队列中获取作业。当它找到一个时,它就完成了工作;如果没有,它会休眠一小段时间,然后醒来并再次尝试。这将降低您的每个任务的开销,但即使没有工作要做,您的线程也会占用 CPU。这完全取决于问题的确切性质。

【讨论】:

  • 问题如下:我有大约 1000 个循环,每个循环大约 10'000 次迭代。这些循环必须按顺序执行,但我有 4 个 CPU 可用。我尝试做的是将 10'000 个迭代循环拆分为 4 个 2'500 个迭代循环,即每个线程一个。但是我必须等待 4 个小循环完成,然后才能进行下一个“大”迭代。这意味着我无法捆绑作业。
【解决方案3】:

注意,在发出 endSignal 后,您仍在要求下一份工作。

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

【讨论】:

  • 感谢您指出这一点。这不是问题,因为 endSignal 是在作业列表为空时调用的,所以它不会得到任何作业并且会正确完成。但你是完全正确的!
【解决方案4】:

由于您说并行执行比顺序执行慢很多,我假设您的内部 2500 次循环迭代的处理时间很短(在几微秒范围内)。然后除了查看算法以拆分更大的进动块之外,您无能为力; OpenMP 无济于事,其他所有同步技术也无济于事,因为它们基本上都依赖于事件(自旋循环不符合条件)。

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

【讨论】:

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

如果您只是并行化循环并使用 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 分析器(它有 2 种有助于查看争用的新并发模式)或 Intel 的 vtune 之类的分析器下查看这个。

您还可以共享您在任务中运行的代码,这样人们就可以更好地了解您在做什么,因为我总是得到的关于性能问题的答案首先是“它取决于”,其次是, “你有没有分析过它。”

祝你好运

-瑞克

【讨论】:

  • 感谢您的回答。我会接受你的,因为你提供了有用的链接并建议使用 OpenMP!
【解决方案6】:

它不应该那么昂贵,但是如果您的工作几乎不需要任何时间,那么线程和同步对象的开销就会变得很大。像这样的线程池对于处理时间较长的作业或那些使用大量 IO 而不是 CPU 资源的作业要好得多。如果您在处理作业时受 CPU 限制,请确保每个 CPU 只有 1 个线程。

可能还有其他问题,getNextJob 是如何获取它的数据来处理的呢?如果有大量的数据复制,那么您的开销又会显着增加。

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

【讨论】:

  • 我和你有同样的优化想法,即让线程在不通过 WaitForMultipleObjects() 的情况下提取作业,但在我的情况下,每个线程的作业很少,所以这不会有太大变化。
  • 我以为你每个线程有 2500 个?没关系 - 另一种方法是检查 OpenMP,它可能更快,而且更容易实现。 (即,您只需在 for 循环之前放置一个编译指示,让它为您管理一切)。
【解决方案7】:

线程之间的上下文切换也很昂贵。在某些情况下,开发一个框架很有趣,您可以使用该框架通过一个线程或多个线程按顺序处理您的作业。这样您就可以两全其美了。

顺便问一下,你的问题到底是什么?我将能够用更准确的问题更准确地回答:)

编辑:

在某些情况下,事件部分可能比您的处理消耗更多,但不应该那么昂贵,除非您的处理速度真的很快。在这种情况下,在 thredas 之间切换也很昂贵,因此我的答案第一部分是按顺序做事......

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

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

如果我猜对了,你的问题是有效地使用你所有的计算机内核/处理器来并行化一些基本顺序的处理。

假设您有 4 个内核和 10000 个循环来计算,如您的示例(在评论中)。您说您需要等待 4 个线程结束才能继续。然后,您可以简化同步过程。你只需要给你的四个线程 thr nth, nth+1, nth+2, nth+3 循环,等待四个线程完成然后继续。您应该使用集合点或屏障(等待 n 个线程完成的同步机制)。 Boost 有这样的机制。您可以查看 windows 实现以提高效率。您的线程池并不真正适合该任务。在关键部分中搜索可用线程会消耗您的 CPU 时间。不是事件部分。

【讨论】:

  • 嗯,我想我的问题是关于使用事件的成本(它们真的很贵还是我做错了?)。
  • neuro 的方法可能是您最好的选择。如果可以的话,您的另一种选择是重新设计循环,使它们不再相互依赖。您可能需要支付性能损失,但这没关系:速度慢 2 倍但随硬件线程数线性扩展的代码总体上获胜,对吗?
【解决方案8】:

看来这整个事件的事情 (连同同步 在线程之间使用关键 部分)非常昂贵!

“昂贵”是一个相对术语。喷气机很贵吗?是汽车吗?还是自行车……鞋子……?

在这种情况下,问题是:相对于 JobFunction 执行所花费的时间,事件是否“昂贵”?发布一些绝对数字会有所帮助:“无线程”时该过程需要多长时间?是几个月,还是几飞秒?

随着线程池大小的增加,时间会发生什么变化?尝试池大小为 1,然后是 2,然后是 4,等等。

此外,由于您过去曾在线程池方面遇到过一些问题,因此我建议您进行一些调试 计算你的线程函数被实际调用的次数......它是否符合你的预期?

从空中挑选一个数字(对您的目标系统一无所知,并假设您没有在未显示的代码中做任何“巨大”的事情),我预计每个人的“事件开销” “工作”以微秒为单位进行测量。也许一百左右。如果在 JobFunction 中执行算法所花费的时间没有明显超过这个时间,那么您的线程可能会花费您而不是节省时间。

【讨论】:

    【解决方案9】:

    如前所述,线程增加的开销取决于执行您定义的“作业”所花费的相对时间量。因此,重要的是要在工作块的大小上找到一个平衡点,以尽量减少块的数量,但不会让处理器空闲等待最后一组计算完成。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-11-16
      • 2019-08-18
      • 2023-03-21
      • 2020-04-08
      • 2010-09-15
      • 2020-07-07
      • 2014-10-30
      • 1970-01-01
      相关资源
      最近更新 更多