我将实现一个基于作业的线程架构。这意味着一方面,主线程可以将作业附加到一个队列。另一方面,有工作线程,取决于可用 CPU 内核的数量,消耗这些作业并将它们从队列中删除。
现在,我想到了两种在 C++ 中实现它的方法。第一个是基于模板的。有一个Task
模板代表一项工作。它拥有一个函数,它可能是一个 lamda 并提供对数据的访问。
要使用它,我们必须在Work
函数对象中存储一些东西,比如 lambda 表达式。此外,我们需要将Data
指针指向我们的数据对象,然后设置Empty
为 false。当然,对象必须附加到作业队列中。获取作业的工作线程将锁定Access
,并且主线程可以每隔一段时间检查锁定以释放以处理结果。
template <class T>
class Task
{
public:
Task()
{
Empty.store(true);
Data = nullptr;
}
std::mutex Access;
std::atomic<bool> Empty;
std::function<void(T*)> Work;
T *Data;
};
第二种方法是基于继承的。空标志和互斥锁与第一种方法一样。但是功函数是一种想要被覆盖的真实方法。此外,我们不再需要数据指针,因为派生任务可以添加它想要的任何成员。
class Task
{
public:
Task()
{
Empty.store(true);
Data = nullptr;
}
std::mutex Access;
std::atomic<bool> Empty;
virtual void Work() = 0;
};
为了更清楚起见,这里有两个简短的示例,说明我将如何从主线程中启动作业。让我们从第一个开始。
int number;
Task<int> *example = new Task<int>();
example.Data = &number;
example.Empty.store(false);
example.Run = [](int* number){
*number = 42;
});
Queue.push_back(example);
对于第二种方法。
class Example : public Task
{
public:
Example(int *number)
{
this->number = number;
}
void Work()
{
*number = 42;
}
int number;
};
int number;
Example *example = new Example(&number);
example.Empty.store(false);
Queue.push_back(example);
这两种方法的性能和灵活性有什么区别?