【问题标题】:What are the differences between those two approaches of threading?这两种线程方法有什么区别?
【发布时间】:2013-08-08 22:44:36
【问题描述】:

我将实现一个基于作业的线程架构。这意味着一方面,主线程可以将作业附加到一个队列。另一方面,有工作线程,取决于可用 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);

这两种方法在性能和灵活性方面有何不同?

【问题讨论】:

  • 为什么不使用英特尔的 TBB(它是免费的、C++、基于任务的多线程并行)?顺便说一句,TBB 使用继承模型,当用户必须重写 tbb::task::execute() 函数时,该函数可以使用 tbb::task 的多个成员,例如生成更多子任务。
  • 既然您似乎在使用 C++ 11,为什么不使用 packaged_task
  • 模板化Task类的原因是什么?这是示例和任务之间“是”关系的一个明显案例
  • @Mgetz 您能否详细说明如何使用std::packaged_task 来获得所描述的行为?

标签: c++ multithreading templates inheritance architecture


【解决方案1】:

第一个示例允许您使用任意线程函数,而无需为其定义全新的类。然而,主要问题是您必须为用户数据分配内存才能将其传递给线程函数。所以即使对于一个只需要一个整数的任务,你仍然需要传递一个指向它的指针。

但是,第二种方法允许您向任务添加任意数量的任意大小的成员,并且还允许您对实际的 Task 实例进行私有访问,这在以后可能会有所帮助。此外,由于它不是模板化的,因此更容易维护Task 实例的列表。

就性能而言,它们几乎相同,因为虚函数只​​是作为函数指针实现的。

【讨论】:

    【解决方案2】:

    继承方法显然是最惯用和最有效的方法。基类Task实现了所有的工作共享和排队等,而用户只需要覆盖纯虚拟成员Work()。这允许任务传播(排队等)的实现独立于任务的实际工作来实现。

    当涉及到多线程应用程序的性能时,虚拟表查找(调用 Task::Work())是您最不必担心的问题。真正的问题是工作队列的竞争条件和子任务的有效传播……另请参阅英特尔的 tbb (http://threadingbuildingblocks.org/)。

    【讨论】:

      猜你喜欢
      • 2013-09-08
      • 2019-03-31
      • 2022-11-18
      • 1970-01-01
      • 1970-01-01
      • 2020-02-14
      • 2011-01-10
      • 2016-07-01
      相关资源
      最近更新 更多