【问题标题】:Thread pools and context switching slowdowns线程池和上下文切换减慢
【发布时间】:2018-04-18 06:39:39
【问题描述】:

在 Windows 应用程序中,我有一个带有空闲线程的线程池,等待将作业推送到队列中。

我的主应用程序线程中有一个循环,它按顺序将 1000 个作业添加到池的队列中(它添加一个作业,然后等待作业完成,然后添加另一个作业,x1000)。所以没有发生实际的并行处理......这里有一些伪代码:

////threadpool:
class ThreadPool
{
    ....

    std::condition_variable job_cv;
    std::condition_variable finished_cv;
    std::mutex job_mutex;
    std::queue<std::function <void(void)>> job_queue;

    void addJob(std::function <void(void)> jobfn)
    {
        std::unique_lock <std::mutex> lock(job_mutex);
        job_queue.emplace(std::move(jobfn));
        job_cv.notify_one();
    }

    void waitForJobToFinish()
    {
        std::unique_lock<std::mutex> lock(job_mutex);
        finished_cv.wait(lock, [this]() {return job_queue.empty(); });
    }

    ....

    void threadFunction() //called by each thread when it's first started
    {
        std::function <void(void)> job;
        while (true)
        {
            std::unique_lock <std::mutex> latch(job_mutex);
            job_cv.wait(latch, [this](){return !job_queue.empty();});

            {
                job = std::move(job_queue.front());
                job_queue.pop();

                latch.unlock();

                job();

                latch.lock();
                finished_cv.notify_one();
            }      
        }
    }
}

...

////main application:

void jobfn()
{
    //do some lightweight calculation
}

void main()
{
    //test 1000 calls to the lightweight jobfn from the thread pool
    for (int q = 0; q < 1000; q++)
    {        
        threadPool->addJob(&jobfn);
        threadPool->waitForJobToFinish(); 
    }
}

所以基本上发生的事情是一个作业被添加到队列中并且主循环开始等待,然后等待线程将其拾取,当线程完成时,它通知应用程序主循环可以继续并且另一个作业可以添加到队列等中。这样可以顺序处理 1000 个作业。

值得注意的是,作业本身很小,可以在几毫秒内完成。

但是,我注意到了一些奇怪的事情......

循环完成所需的时间基本上是 O(n),其中 n 是线程池中的线程数。因此,即使在所有场景中一次处理一个作业,10 线程池完成完整的 1000 个作业任务所需的时间比 1 线程池长 10 倍。

我试图找出原因,到目前为止我唯一的猜测是上下文切换是瓶颈......当只有 1 个线程正在抓取作业时,可能需要更少(或零?)上下文切换开销......但是当 10 个线程不断轮流一次处理一个作业时,是否需要一些额外的处理?但这对我来说没有意义......这不是为工作解锁线程 A 所需的相同操作,因为它是线程 B、C、D ......?是否正在进行一些操作系统级别的缓存,其中一个线程不会丢失上下文,直到给它一个不同的线程?所以一遍又一遍地调用同一个线程比依次调用线程 A、B、C 更快?

但在这一点上这是一个完整的猜测......也许其他人可以了解我为什么会得到这些结果......直觉上我假设只要一次只执行一个线程,我就可以有一个线程池具有任意数量的线程,并且 [x] 个作业的总任务完成时间将相同(只要每个作业相同且作业总数相同)...为什么会这样错误的?

【问题讨论】:

  • 这可能不相关,但是你有多少个核心?
  • @merlin2011 16(2 CPU x 8 核)。
  • 另外,您如何对此进行基准测试?也就是说,您是在开始和结束时测量时间,还是收集每个任务的数据,以便判断是否每 10 次中就有 1 次超慢?
  • @merlin2011 我只在开始和结束时进行测量......任务本身完成得非常快,所以我不确定单个任务测量的准确度......虽然我没有想想减速是否可能只发生在某些频率...
  • 这纯粹是推测(因为我无法在没有完整示例的情况下对您的代码进行基准测试),但是当您拥有大量线程时,您可能偶尔会跨越套接字边界,这将显示每隔几秒钟就会成为一项昂贵的工作。如果您想准确测量单个任务并且您使用的是 Intel,则可以使用 rdtsc 指令。您可以从this library 获取一个方便的包装器。

标签: c++ multithreading switch-statement


【解决方案1】:

你的“猜测”是正确的;这只是一个资源争用问题。

您的 10 个线程不是空闲的,它们正在等待。这意味着操作系统必须为您的应用程序迭代当前活动的线程,这意味着可能会发生上下文切换。

活动线程被推回,一个“等待”线程被拉到前面,其中代码检查是否已通知信号并且可以获取锁,因为它可能无法在时间片内线程,它继续迭代剩余的线程,每个线程都试图查看是否可以获取锁,但它不能,因为您的“活动”线程还没有被分配一个时间片来完成。

单线程池没有这个问题,因为在操作系统级别不需要迭代额外的线程;当然,单线程池仍然比调用job 1000 次要慢。

希望能有所帮助。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-12-05
    • 2017-09-09
    • 2011-07-23
    • 1970-01-01
    • 1970-01-01
    • 2016-06-18
    • 2011-07-27
    相关资源
    最近更新 更多