C++ C++;:线程池比单线程慢?

C++ C++;:线程池比单线程慢?,c++,multithreading,C++,Multithreading,首先,我查看了本网站上的其他主题,发现它们与我的问题无关,因为这些主题主要涉及使用I/O操作或线程创建开销的人。我的问题是,我的线程池或辅助任务结构实现(在本例中)比单线程慢得多。我真的被这搞糊涂了,不确定是线程池、任务本身、我如何测试它、线程的性质还是我无法控制的东西 // Sorry for the long code #include <vector> #include <queue> #include <thread> #include <mu

首先,我查看了本网站上的其他主题,发现它们与我的问题无关,因为这些主题主要涉及使用I/O操作或线程创建开销的人。我的问题是,我的线程池或辅助任务结构实现(在本例中)比单线程慢得多。我真的被这搞糊涂了,不确定是线程池、任务本身、我如何测试它、线程的性质还是我无法控制的东西

// Sorry for the long code
#include <vector>
#include <queue>

#include <thread>
#include <mutex>
#include <future>

#include "task.hpp"

class ThreadPool
{
public:
    ThreadPool()
    {
        for (unsigned i = 0; i < std::thread::hardware_concurrency() - 1; i++)
            m_workers.emplace_back(this, i);

        m_running = true;
        for (auto&& worker : m_workers)
            worker.start();
    }
    ~ThreadPool()
    {
        m_running = false;
        m_task_signal.notify_all();
        for (auto&& worker : m_workers)
            worker.terminate();
    }

    void add_task(Task* task)
    {
        {
            std::unique_lock<std::mutex> lock(m_in_mutex);
            m_in.push(task);
        }
        m_task_signal.notify_one();
    }
private:
    class Worker
    {
    public:
        Worker(ThreadPool* parent, unsigned id) : m_parent(parent), m_id(id)
        {}
        ~Worker()
        {
            terminate();
        }

        void start()
        {
            m_thread = new std::thread(&Worker::work, this);
        }
        void terminate()
        {
            if (m_thread)
            {
                if (m_thread->joinable())
                {
                    m_thread->join();
                    delete m_thread;
                    m_thread = nullptr;
                    m_parent = nullptr;
                }
            }
        }
    private:
        void work()
        {
            while (m_parent->m_running)
            {               
                std::unique_lock<std::mutex> lock(m_parent->m_in_mutex);
                m_parent->m_task_signal.wait(lock, [&]()
                {
                    return !m_parent->m_in.empty() || !m_parent->m_running;
                });

                if (!m_parent->m_running) break;
                Task* task = m_parent->m_in.front();
                m_parent->m_in.pop();
                // Fixed the mutex being locked while the task is executed
                lock.unlock();

                task->execute();            
            }
        }
    private:
        ThreadPool* m_parent = nullptr;
        unsigned m_id = 0;

        std::thread* m_thread = nullptr;
    };
private:
    std::vector<Worker> m_workers;

    std::mutex m_in_mutex;
    std::condition_variable m_task_signal;
    std::queue<Task*> m_in;

    bool m_running = false;
};

class TestTask : public Task
{
public:
    TestTask() {}
    TestTask(unsigned number) : m_number(number) {}

    inline void Set(unsigned number) { m_number = number; }

    void execute() override
    {
        if (m_number <= 3)
        {
            m_is_prime = m_number > 1;
            return;
        }
        else if (m_number % 2 == 0 || m_number % 3 == 0)
        {
            m_is_prime = false;
            return;
        }
        else
        {
            for (unsigned i = 5; i * i <= m_number; i += 6)
            {
                if (m_number % i == 0 || m_number % (i + 2) == 0)
                {
                    m_is_prime = false;
                    return;
                }
            }
            m_is_prime = true;
            return;
        }
    }
public:
    unsigned m_number = 0;
    bool m_is_prime = false;
};

int main()
{
    ThreadPool pool;

    unsigned num_tasks = 1000000;
    std::vector<TestTask> tasks(num_tasks);
    for (auto&& task : tasks)
        task.Set(randint(0, 1000000000));

    auto s = std::chrono::high_resolution_clock::now();
    #if MT
    for (auto&& task : tasks)
        pool.add_task(&task);
    #else
    for (auto&& task : tasks)
        task.execute();
    #endif
    auto e = std::chrono::high_resolution_clock::now();
    double seconds = std::chrono::duration_cast<std::chrono::nanoseconds>(e - s).count() / 1000000000.0;
}
这类回答中的常见免责声明:唯一确定的方法是使用探查器工具进行测量

但我会尽力解释你的结果。首先,在所有线程中都有一个互斥体。因此,一次只能有一个线程执行某些任务。它扼杀了你可能获得的所有收益。尽管有线程,您的代码仍然是完全串行的。因此,至少让您的任务执行脱离互斥锁。您只需要锁定互斥体就可以将任务从队列中取出,而不需要在任务执行时保持它

其次,您的任务非常简单,单线程将很快执行它们。你无法衡量这些任务的任何收益。创建一些繁重的任务,可以产生一些更有趣的结果(一些任务更接近真实世界,而不是人为的)

第三点:线程并非没有成本-上下文切换、互斥争用等。要获得真正的收益,正如前两点所说,您需要有比线程引入的开销花费更多时间的任务,并且代码应该是真正并行的,而不是等待某些资源使其串行。

UPD:我看了错误的代码部分。如果创建的任务数量足够大,那么任务就足够复杂


UPD2:我已经使用了您的代码,并找到了一个好的素数来显示MT代码如何更好。使用以下素数:1019048297。它将提供足够的计算复杂性来显示差异


但是为什么你的代码不能产生好的结果呢?如果没有看到
randint()
的实现,很难判断,但我认为它非常简单,在一半的情况下,它返回偶数,而其他情况下也不会产生太多大的素数。因此,这些任务非常简单,上下文切换以及围绕特定实现和线程的其他事情通常比计算本身花费更多的时间。使用我给你的素数,让任务别无选择,只能花时间计算——这不是一个简单的答案,因为这个数字很大,实际上是素数。这就是为什么大数字会给你你寻找的答案-更好的时间为机器翻译代码

在执行任务时不应持有互斥锁,否则其他线程将无法获取任务:

void work() {
    while (m_parent->m_running) {   
        Task* currentTask = nullptr;    
        std::unique_lock<std::mutex> lock(m_parent->m_in_mutex);
        m_parent->m_task_signal.wait(lock, [&]() {
            return !m_parent->m_in.empty() || !m_parent->m_running;
        });                     
        if (!m_parent->m_running) continue;
        currentTask = m_parent->m_in.front();
        m_parent->m_in.pop();               
        lock.unlock(); //<- Release the lock so that other threads can get tasks
        currentTask->execute();
        currentTask = nullptr;
    }   
}       
void work(){
当(m_parent->m_running){
Task*currentTask=nullptr;
std::unique_lock锁(m_parent->m_in_mutex);
m_parent->m_task_信号。等待(锁定,[&](){
return!m_parent->m_in.empty()| |!m_parent->m_running;
});                     
如果(!m_parent->m_running)继续;
currentTask=m_parent->m_in.front();
m_parent->m_in.pop();
lock.unlock();//执行();
currentTask=nullptr;
}   
}       

对于MT,在“开销”的每个阶段花费了多少时间:
std::unique_lock
m_task_signal.wait
front
pop
解锁

根据你只做了3%有用工作的结果,这意味着上面的工作消耗了97%。我会得到上面每个部分的号码(例如,在每次通话之间添加时间戳)

在我看来,用于[仅仅]将下一个任务指针出列的代码相当繁重。我会使用更简单的队列(可能是无锁的)机制。或者,也许可以使用原子将索引插入队列,而不是上面的五步过程。例如:

void
work()
{
    while (m_parent->m_running) {
        // NOTE: this is just an example, not necessarily the real function
        int curindex = atomic_increment(&global_index);
        if (curindex >= max_index)
            break;

        Task *task = m_parent->m_in[curindex];

        task->execute();
    }
}
另外,也许你应该一次弹出十个,而不是一个

您可能还受到内存限制和/或“任务切换”限制。(例如)对于访问阵列的线程,通常有四个以上的线程使内存总线饱和。您还可能会对锁产生严重的争用,这样线程就会饿死,因为一个线程正在独占锁[间接地,即使使用新的
unlock
调用]

线程间锁定通常涉及“序列化”操作,其中其他内核必须同步其无序执行管道

下面是一个“无锁”实现:

void
work()
{
    // assume m_id is 0,1,2,...
    int curindex = m_id;

    while (m_parent->m_running) {
        if (curindex >= max_index)
            break;

        Task *task = m_parent->m_in[curindex];

        task->execute();

        curindex += NUMBER_OF_WORKERS;
    }
}

首先展示你的“耗时”代码,你如何进行度量,以及你如何编译它。“可能很重要。”我发现那个错误太晚了。修改后的答案。有多少核?如果只有一个,多线程代码很容易比单线程代码慢。您的程序不会改变环境(stdio、文件系统、网络、渲染等)。从理论上讲,过于智能的编译器可以将程序优化到无操作或接近操作(因为它不会改变可观察的结果)。你能发布ST variant的
main
反汇编吗?在MT版本中,你不应该等待任务完成吗?你有没有不太难完成的繁重任务?@James,请参阅更新的答案。我看错了你的代码我已经解决了互斥问题,并使用分析器更新了我看到的基准测试。。。我最初的目标是在我正在从事的另一个项目中卸载一个ST任务。我会看看这个函数是否有价值。我刚从ixSci修复了这个answer@James这很好,只需确保将帮助您解决问题的答案标记为已接受即可。我有相同的想法,可以推送4个(或任意多个)任务,通知所有4个线程并获取一组任务。我有点回避原子学,因为我还没有完全了解它们,但它们似乎比原子学更好
void
work()
{
    // assume m_id is 0,1,2,...
    int curindex = m_id;

    while (m_parent->m_running) {
        if (curindex >= max_index)
            break;

        Task *task = m_parent->m_in[curindex];

        task->execute();

        curindex += NUMBER_OF_WORKERS;
    }
}