代码之家  ›  专栏  ›  技术社区  ›  Wookai

由于使用事件而产生的开销

  •  6
  • Wookai  · 技术社区  · 16 年前

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

    问题如下:我有大约1000个循环,每个循环大约10000次迭代。这些循环必须按顺序执行,但我有4个CPU可用。我试图做的是将10000个迭代循环拆分为42000个迭代循环,即每个线程一个循环。但我必须等待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));
    }
    
    9 回复  |  直到 16 年前
        1
  •  3
  •   Cătălin Pitiș    16 年前

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

    here .

        2
  •  3
  •   David Seiler    16 年前

    WaitForMultipleObjects 它相当昂贵。如您所见,如果您的作业很小,那么同步开销将开始超过实际执行该作业的成本。

    解决这一问题的一种方法是将多个作业捆绑成一个:如果你得到一个“小”作业(不管你如何评估这些东西),将其存储在某个地方,直到你有足够多的小作业组合在一起,形成一个合理大小的作业。然后将它们全部发送到工作线程进行处理。

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

        3
  •  2
  •   TimW    16 年前

    注意,在发出结束信号后,您仍在要求下一份工作。

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

    既然你说是

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

        5
  •  1
  •   gbjbaanb    16 年前

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

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

        6
  •  1
  •   neuro    16 年前

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

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

    编辑:

    您应该查找线程间同步瓶颈。您可以跟踪线程等待时间,以。。。

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

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

    假设您有4个内核和10000个循环要计算,如您的示例(在注释中)。您说过需要等待4个线程结束后才能继续。然后,您可以简化同步过程。你只需要给你的四个线程thr n,n+1,n+2,n+3个循环,等待这四个线程完成,然后继续。您应该使用集合或屏障(等待n个线程完成的同步机制)。 Boost

        7
  •  1
  •   Roddy    16 年前

    看来整件事 (与同步 在线程之间使用临界值 (部分)是相当昂贵的!

    “昂贵”是一个相对的术语。飞机贵吗?是汽车吗?或者自行车。。。鞋

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

    随着线程池大小的增加,时间会发生什么变化?尝试将池大小设置为1、2、4,以此类推。

    另外,由于您过去在这里遇到了一些线程池问题,我建议您进行一些调试 要计算实际调用threadfunction的次数。。。它符合你的期望吗?

        8
  •  1
  •   Rick    16 年前

    如果您只是在并行循环并使用VS2008,我建议您看看OpenMP。如果您使用的是visual studio 2010 beta 1,我建议您查看 parallel pattern library ,尤其是 "parallel for" / "parallel for each" apis 或者 "task group 类,因为它们很可能会执行您试图执行的操作,只需要更少的代码。

    我建议你在一个像 xperf visual studio 2010 beta 1中的f1探查器(它有两种新的并发模式,有助于查看争用)或Intel的vtune。

    您还可以共享您在任务中运行的代码,这样人们就可以更好地了解您在做什么,因为对于性能问题,我总是得到的答案是第一个“这取决于”第二个“您是否分析了它”

    祝你好运

    -瑞克

        9
  •  0
  •   Jim Monte    8 年前

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