首先,我确实查看了本网站上的其他主题,发现它们与我的问题无关,因为这些主要涉及使用I / O操作或线程创建开销的人。我的问题是我的线程池或工作者任务结构实现(在这种情况下)比单线程慢很多。我真的很困惑,不确定它是ThreadPool,任务本身,我如何测试它,线程的性质或我无法控制的东西。
// 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;
}
使用VS2013 Profiler进行基准测试:
10,000,000 tasks:
MT:
13 seconds of wall clock time
93.36% is spent in msvcp120.dll
3.45% is spent in Task::execute() // Not good here
ST:
0.5 seconds of wall clock time
97.31% is spent with Task::execute()
答案 0 :(得分:6)
此类答案中的通常免责声明:唯一可以确定的方法是使用分析器工具进行测量。
但我会尝试在没有它的情况下解释你的结果。首先,你的所有线程都有一个互斥锁。因此,一次只有一个线程可以执行某些任务。它会杀死你可能获得的所有收益。尽管你的线程,你的代码完全是串行的。所以至少要让你的任务执行来自互斥锁。您只需锁定互斥锁即可将任务从队列中取出 - 您无需在任务执行时保留该任务。
接下来,您的任务非常简单,单个线程就可以立即执行它们。你无法通过这些任务衡量任何收益。创建一些繁重的任务,可以产生一些更有趣的结果(一些更接近现实世界的任务,而不是这样的设计)。
第三点:线程并非没有成本 - 上下文切换,互斥争用等。要获得真正的收益,如前两点所说,你需要花费更多时间的任务比线程引入的开销和代码应该是真正并行的,而不是等待一些资源使它串行。
UPD:我查看了错误的代码部分。如果您创建的数字足够大,任务就足够复杂了。
UPD2 :我已经使用了您的代码并找到了一个很好的素数来展示MT代码是如何更好的。使用以下素数:1019048297.它将提供足够的计算复杂度来显示差异。
但为什么你的代码没有产生好的结果呢?没有看到randint()
的实现很难说,但我认为它很简单,在一半的情况下,它返回偶数和其他情况也不会产生太大的素数。因此,任务非常简单,以至于上下文切换以及特定实现和线程周围的其他事情通常比计算本身消耗更多时间。使用素数,我给你的任务别无选择,只花时间计算 - 没有简单的答案,因为数字很大,实际上是素数。这就是为什么大数字会给你你寻求的答案 - 为MT代码提供更好的时间。
答案 1 :(得分:2)
在任务执行时你不应该持有互斥锁,否则其他线程将无法获得任务:
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;
}
}
答案 2 :(得分:1)
对于MT,在&#34;开销的每个阶段花费了多少时间&#34;:std::unique_lock
,m_task_signal.wait
,front
,pop
,{ {1}}?
根据您仅有3%有用工作的结果,这意味着上述消耗了97%。我会为上面的每一部分获取数字(例如,在每次通话之间添加时间戳)。
在我看来,用于[仅]出列下一个任务指针的代码非常繁重。我做了一个更简单的队列[可能无锁]机制。或者,也许,使用atomics将索引压入队列而不是上面的五步过程。例如:
unlock
另外,也许你应该一次弹出[说]十个而不是一个。
您也可能受内存限制和/或&#34;任务切换&#34;界。 (例如)对于访问阵列的线程,超过四个线程通常会使内存总线饱和。你也可能有很大的争用锁,因为一个线程正在垄断锁[间接,即使是新的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();
}
}
调用]
线程锁定通常涉及&#34;序列化&#34;其他核心必须同步其无序执行流水线的操作。
这是一个&#34;无锁&#34;实现:
unlock