工厂 - 处理器模型

时间:2015-10-28 08:25:58

标签: c++ concurrency

工厂以异步方式向处理器提供不同类型的任务。处理器不知道任务的细节并通过已知接口执行它们。由于性能原因,禁止动态分配。工厂不应该拥有任务,否则当处理器完成执行任务以进行清理时,处理器需要通知工厂。处理器应该只知道接口,但不知道任务本身。处理器可以在处理任务时将任务作为不透明对象。

一种可能的解决方案是:将所有类型的任务存储在“接口和填充缓冲区”的并集中。请考虑以下工作示例(C ++ 11):

#include <iostream>

struct Interface
{
    virtual void execute() {}
};

union X
{
    X() {}
    Interface i;
    char padding[1024];
    template <class T>
    X& operator= (T &&y)
    {
        static_assert (sizeof(T) <= sizeof(padding), "X capacity is not enough!");
        new (padding) T(y);
    }
};

struct Task : public Interface
{
    Task() : data(777) {}
    virtual void execute() { std::cout << data << std::endl; }
    int data;
};

int main()
{
    Task t;
    X x;
    x = std::move(t);
    Interface *i = &x.i;
    i->execute();
};

该代码段效果很好(打印777)。但是这种方法有没有危险(比如虚拟继承)?也许有更好的解决方案吗?

2 个答案:

答案 0 :(得分:0)

更新了答案。

请参阅:std::aligned_union (en.cppreference.com)。它被设计为与放置新的和显式的析构函数调用一起使用。

以下是较早的答案,现已缩回。

从设计角度来看,

  • 避免动态分配似乎是一项严苛的要求。这需要一些非常合理的理由。
  • 如果一个人因任何原因不信任标准分配器,人们仍然可以实现自定义分配器,以便完全控制其行为。
  • 如果有一个类或方法“拥有”所有实例:所有工厂,所有处理器和所有任务(如main()方法中的情况),则无需复制任何东西。只需传递引用或指针,因为这个“拥有所有东西的类或方法”将处理对象的生命周期。

我的回答仅适用于有关“memcpy”的问题。

我不会尝试解决“具有基类的接口的任务”和“具有成员的接口的X”之间的memcpy问题。对于所有C ++编译器来说,这似乎并不普遍有效,但我不知道哪些C ++编译器会使这段代码失败。

简短回答,适用于所有C ++编译器:

目前,平凡的可复制列表“没有虚函数”作为必要条件之一,因此“根据规范”的答案是你的结构Task不是轻易复制的。

更长,非标准的答案是你的特定编译器是否会合成结构和机器代码,它们有效可复制(即没有不良影响),尽管C ++规范说不。显然,这个答案将是特定于编译器的,并且将取决于许多情况(例如优化标志和次要代码更改)。

请记住,编译器优化和代码生成可以在不同版本之间进行更改。无法保证下一版本的编译器的行为完全相同。

举例说明在两个实例之间记忆可能不安全的事情,请考虑:

struct Task : public Interface
{
    Task(std::string&& s) 
        : data(std::move(s)) 
    {}
    virtual void execute() { std::cout << data << std::endl; }
    std::string data;
};

这有问题的原因是,对于足够长的字符串,std::string将分配动态内存来存储其内容。如果有Task的两个实例,并且memcpy用于将其字节从一个实例复制到另一个实例(它将复制到std::string类的内部字段),则它们的指针将指向相同的地址,因此他们的析构函数都会尝试删除相同的内存,导致未定义的行为。此外,如果被覆盖的实例具有较早的字符串值,则不会释放内存。

由于你说“禁止动态分配”,我的猜测是你不会使用std::string或类似的东西,而是选择专门编写类似C的代码。所以这个问题可能与你无关。

说到“低级C代码”,这是我的想法:

struct TaskBuffer
{
    typedef void (*ExecuteFunc) (TaskBuffer*);
    ExecuteFunc executeFunc;
    char padding[1024];
};

void ProcessMethod(TaskBuffer* tb)
{
    (tb->executeFunc)(tb);
}

答案 1 :(得分:0)

您的解决方案似乎涉及不必要的复制操作,并且假设您的对象在内存中的布局在所有情况下都不能保证是正确的。它通过使用memcpy使用虚方法复制对象来进一步调用未定义的行为,这是c ++规范明确禁止的。它还有可能引起对象析构函数运行时的混淆。

我会使用这样的安排:

class Processor有一个缓冲区数组,每个缓冲区都足够大,可以包含任务接口的任何已定义子类。它有两种用于提交任务的方法:

  • 一个返回指向当前可用缓冲区的指针
  • 一个提交工作

作业接口扩展时需要跟踪指向包含它的缓冲区的指针(将作为构造函数参数提供),并且有一个返回该指针的方法。

现在提交新任务就像这样:

void * buffer = processor.getBuffer();
Task * task = new (buffer) Task(buffer);
processor.submitJob(task);

(如果需要,可以使用Processor中的模板方法简化)。然后,处理器只执行作业,当它完成后,它会询问它们的缓冲区,运行它们的析构函数,然后将缓冲区添加回其空闲缓冲区列表中。