5

我正在开发一个实时系统,我正在讨论课程的设计。
具体来说,我无法决定是否使用两阶段构建 来构建“重”类。

一方面,调用“重”类的构造函数可能是运行时的主要瓶颈,它使我免于创建类和分配用户可能不会使用的功能的内存。

另一方面,考虑到我们尝试访问一项能力但由于它没有初始化而无法访问的情况,两阶段构建可能会在执行过程中产生意外,突然我们需要在使用前完全构建它。

我倾向于采用两阶段施工方法。我喜欢听到的是实时系统两阶段构建的优点\缺点。如果有更好的方法来解决这个问题。

这是一个重类的代码示例(我的类肯定不会像那样,但它证明了我的想法):

 class VeryHeavy {

 private:

    HeavyClass1* p1;
    HeavyClass2* p2;
    HeavyClass3* p3;
    HeavyClass4* p4;
    HeavyClass5* p5;

    int* hugeArray [100000];

    //...//

};
4

3 回答 3

21

在此处输入图像描述

这是 AGC,阿波罗导航计算机,用于阿波罗指令舱和登月舱。以几乎导致阿波罗 11 号着陆被擦洗而闻名。就在下降到月球表面的过程中,这台计算机因实时错误而崩溃。几次。产生系统错误 1201(执行溢出 - 没有空白区域)和系统错误 1202(执行溢出 - 没有核心集)。Armstrong 和 Aldrin 只看到了数字,您在照片右侧看到的 UI 设备太原始而无法显示字符串。是指导控制器史蒂夫贝尔斯知道这些数字的含义(他们在训练时从未见过错误)并且知道系统可以从中恢复。无论如何,通过给出 GO 拯救了着陆,他因此获得了总统自由勋章。

这可能是您的问题所要问的,尽管我们可以很确定您并没有试图让火箭着陆。“实时”这个词过去在软件工程中定义得很好,但被金融业弄糊涂了。在 Apollo 11 中,这意味着一个系统对外部事件的最大响应时间有一个非常严格的上限。火箭需要这样一个系统,有时候调整喷口也不能太晚,迟到会产生十亿美元的火球。金融业将其劫持为一个任意的系统,有时迟到不会使机器蒸发,尽管它使交易损失的可能性更大。他们可能也认为这是一场灾难:)

您使用的内存分配器很重要,也没有在问题中定义。我任意假设您的程序在按需分页的虚拟内存操作系统上运行。不完全是实时系统的理想环境,但足够普遍,真正的实时操作系统表现不佳。

两阶段构造是一种用于处理初始化失败的技术,构造函数中抛出的异常很难处理,析构函数不会运行,因此如果在构造函数中分配而不使构造函数足够聪明,可能会导致资源泄漏来处理一个意外。另一种方法是稍后在成员函数内部进行,根据需要延迟分配。

所以你担心的是惰性分配会阻碍系统的响应能力。产生系统错误 1201。

这实际上不是 Linux 或 Windows 等按需分页虚拟内存操作系统的主要问题。这些操作系统上的内存分配器速度很快,它只分配虚拟内存。不需要任何费用,它是虚拟的。真正的成本出现在以后,当您真正开始使用分配的内存时。需求分页的“需求”发挥作用的地方。寻址数组元素将产生页面错误,迫使操作系统将寻址的虚拟内存页面映射到 RAM。这种页面错误相对便宜,称为“软”页面错误,如果机器没有处于压力之下并且必须取消映射另一个进程正在使用的页面来获取 RAM。你会期望操作系统能够抓取一个页面并映射它,

所以实际上,如果你做对了,并且在分配它时不尝试初始化整个数组,那么你的程序将受到数以万计的小针刺的开销。每一个都足够小,不会危及实时响应保证。不管你是提前分配内存还是延迟分配内存都会发生这种情况,所以是否使用两阶段构造无关紧要。

如果你想保证这也不会发生,或者想在初始化整个数组时对页面错误的风暴有弹性,那么你需要一种非常不同的方法,你需要页面锁定RAM 分配,以便操作系统无法取消映射页面。这总是需要修改操作系统设置,它通常不允许进程页面锁定大量内存。当然,两阶段建设也是不可能的。

请记住,程序很少知道如何处理分配失败。它们的行为几乎像异步异常一样,随时准备在程序的几乎任何部分中随时触发。尤其难以与实时要求相协调,因为内存不足而对实时事件没有响应的系统当然不比迟到的系统好。那仍然是一个火球;)因此,这本身就应该已经有足够的理由不打扰两阶段构造,只需在程序初始化时分配内存,然后再开始承诺实时响应。它使程序编码变得更加简单,失败的几率要低得多。

对于任何具有实时特性的软件来说,一个相当硬的要求是它不必与其他进程竞争来获取操作系统资源。预计整个机器只用于一个进程,您不再像 AGC 那样受限于 36864 字的绳索内存和 2048 字的 RAM。如今,硬件足够便宜且足够丰富,可以提供这样的保证。

于 2013-07-27T22:47:55.487 回答
2

Hans Passant 的回答深刻地描述了为什么您应该在“实时”要求下尝试不使用延迟初始化。

但是如果你真的需要“懒惰”,你应该尽量不要给类用户和实现者带来重复的负担if(!is_constructed) construct();

首先,考虑廉价的默认构造,例如std::vector

vector<int> x;

它构造空向量。并且,例如,您可以安全地调用begin(x)and end(x)- 在这个意义上,对象是有效的,并且是构造的。

但是,如果你的类真的必须在构造函数中做繁重的工作,并且你想在第一次使用之前避免它,那么考虑制作可重用的非侵入式惰性初始化器 - 它会在第一次使用时自动进行初始化,而不强制用户和实现者做样板检查。

这是可能的用法:

struct Widget
{
    Widget(int x)
    {
        cout << "Widget(" << x << ")" << endl;
    }
    void foo()
    {
        cout << "Widget::foo()" << endl;
    }
};

int main()
{
    auto &&x = make_lazy<Widget>(11);
    cout << "after make_lazy" << endl;
    x->foo();
}

输出是:

after make_lazy
Widget(11)
Widget::foo()

现场演示

#include <boost/utility/in_place_factory.hpp>
#include <boost/optional.hpp>
#include <iostream>
#include <utility>

using namespace boost;
using namespace std;

template<typename T, typename Factory>
class Lazy
{
    mutable optional<T> x;
    Factory f;

    T *constructed() const
    {
        if(!x) x = f;
        return &*x;
    }
public:
    Lazy(Factory &&f) : f(f) {}

    T *operator->()
    {
        return constructed();
    }
    const T *operator->() const
    {
        return constructed();
    }
};

template<typename T, typename ...Args>
auto make_lazy(Args&&... args) -> Lazy<T, decltype(in_place(forward<Args>(args)...))>
{
    return {in_place(forward<Args>(args)...)};
}

/*****************************************************/

struct Widget
{
    Widget(int x)
    {
        cout << "Widget(" << x << ")" << endl;
    }
    void foo()
    {
        cout << "Widget::foo()" << endl;
    }
};

int main()
{
    auto &&x = make_lazy<Widget>(11);
    cout << "after make_lazy" << endl;
    x->foo();
}
于 2013-07-30T20:58:10.547 回答
1

如果我们有 2 个实体,则两阶段方法的主要“专业”。第一个提供接口 IFirst 并需要外部 ISecond 实现。第二个提供 ISecond 并依次需要 IFirst。没有两阶段初始化,这是“鸡和蛋”的未解决问题。

根据重对象与有限范围(如实时/移动/嵌入式),将对象做得尽可能薄和懒惰可能是值得的。潜在地,在使用某些功能之前提供一系列 init 调用可能是调用者的责任,只是为了确保在启动之前一切都以正确的方式初始化。

于 2013-07-25T11:18:47.333 回答