我将实现基于作业的线程架构。这意味着一方面,主线程可以将作业附加到队列。另一方面,工作线程依赖于可用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);
这两种方法的性能和灵活性有何不同?
答案 0 :(得分:2)
第一个示例允许您使用任意线程函数,而无需为其定义全新的类。但是,主要问题是必须为用户数据分配内存以将其传递给线程函数。所以即使是一个只需要取整数的任务,你仍然需要传递一个指向它的指针。
然而,第二种方法允许您向任务添加任意大小的任意数量的成员,并且还允许您私有访问实际的Task
实例,这可能在以后有益。此外,因为它没有模板化,所以维护Task
实例列表更容易。
就性能而言,它们几乎相同,因为虚函数只是作为函数指针实现。
答案 1 :(得分:1)
继承方法显然是最惯用和最有效的方式。基类Task
实现所有工作共享和排队等,而用户只需覆盖纯虚拟成员Work()
。这样就可以独立于任务的实际工作来实现任务传播(排队等)的实现。
虚拟表查找(call Task::Work()
)是您在多线程应用程序性能方面最不用担心的问题。真正的问题是工作队列的竞争条件和子任务的有效传播......另见英特尔的tbb(http://threadingbuildingblocks.org/)。