我正在开发一个实时系统,我正在讨论这些课程的设计 具体来说,我无法通过使用两阶段构造来决定是否构建“重”类。
一方面,在运行时调用“heavy”类的构造函数可能是一个主要的瓶颈,它使我免于创建类和分配记忆用户可能不会使用的功能。
另一方面,考虑到我们尝试访问一种能力的情况,两阶段构造可能会在执行期间出现意外,但我们不能因为它没有初始化,突然我们需要在之前完全构建它使用
我倾向于采用两阶段施工方法。我喜欢听到的是实时系统中两阶段构建的优点。如果有更好的方法来解决这个问题。
这是一个重型课程的代码示例(我的课肯定不会那样,但它证明了我的想法):
class VeryHeavy {
private:
HeavyClass1* p1;
HeavyClass2* p2;
HeavyClass3* p3;
HeavyClass4* p4;
HeavyClass5* p5;
int* hugeArray [100000];
//...//
};
答案 0 :(得分:21)
这是AGC,阿波罗指导计算机,用于Apollo命令模块和月球模块。着名的几乎导致阿波罗11号着陆被擦洗。在下降到月球表面的中间,这台计算机因实时错误而崩溃。几次。产生系统错误1201(执行溢出 - 没有空白区域)和系统错误1202(执行溢出 - 没有核心集)。阿姆斯特朗和奥尔德林只看到了数字,你在照片右边看到的UI设备太原始了,无法显示字符串。指导控制器Steve Bales知道数字意味着什么(他们在训练时从未见过错误),并且知道系统可以从中恢复。无论如何,通过给予GO来保存着陆,他获得了总统自由勋章。
这可能就是你的问题所要求的,虽然我们可以肯定你并没有试图降落火箭。术语"实时"过去在软件工程中有很好的定义,但它被金融业混淆了。在Apollo 11中,它意味着一个系统对外部事件的最大响应时间有一个非常严格的上限。火箭队需要一个这样的系统,在调整喷嘴时有时候太晚,迟到一次会产生十亿美元的火球。金融业劫持它意味着一个任意快速的系统,迟到有时候不会使机器汽化,尽管它会使交易损失的可能性更大。他们可能也认为这也是灾难:)
你使用的内存分配器很重要,也没有在问题中定义。我随意地假设您的程序在需求分页的虚拟内存操作系统上运行。不完全是实时系统的理想环境,但通常情况下,真正的实时操作系统并不是很好。
两阶段构造是一种用于处理初始化失败的技术,构造函数中抛出的异常很难处理,析构函数不会运行,因此如果在构造函数中分配,则可能导致资源泄漏建设者聪明到足以应对不幸事故。另一种方法是稍后在成员函数内执行,根据需要懒惰地分配。
所以你担心的是,懒惰分配会妨碍系统的响应能力。产生系统错误1201。
这实际上并不是Linux或Windows等需求页面虚拟内存操作系统的主要问题。这些操作系统上的内存分配器速度很快,只分配虚拟内存。哪个不花钱,它是虚拟的。当您实际开始使用分配的内存时,真正的成本会降低。在哪里"要求"需求分页发挥作用。寻址数组元素会产生页面错误,迫使操作系统将寻址的虚拟内存页映射到RAM中。这样的页面错误相对便宜,称为“软”和“#34;页面错误,如果机器没有其他压力,必须取消映射另一个进程正在使用的页面以获取RAM。您希望操作系统能够抓取页面并映射它,开销以微秒为单位。
因此,实际上,如果你做得对,并且在分配时不要尝试初始化整个阵列,那么你的程序将受到成千上万的小开销。每一个小到足以不危及实时响应保证。无论您是提前还是延迟分配内存,都会发生这种情况,因此无论您使用两阶段构建都不重要。
如果你想保证这不会发生,或者想要适应初始化整个阵列时遇到的页面错误风暴,那么你需要一种非常不同的方法,您需要页面锁定RAM分配,以便操作系统无法取消映射页面。这总是需要修改操作系统设置,它通常不允许进程对大量内存进行页面锁定。当然,两相结构也是不可能的。
请记住,程序很少知道如何处理分配失败。它们像异步异常一样几乎,随时准备在程序的几乎任何部分中的任何时间点击。特别难以与实时要求协调一致,因为内存不足而对实时事件做出 no 响应的系统当然不会比那个迟到的系统好。这仍然是一个火球;)因此,本身应该已经足够理由不打扰两阶段构造,只需在程序初始化时分配内存,然后再开始承诺实时响应。它使程序编码 lot 更简单,失败的几率要低得多。
对于任何具有实时特性的软件来说,一个非常困难的要求是它不必与其他进程争夺获取操作系统资源。预计将整个机器专用于一个进程,您不再局限于rope memory的36864个字和AGC的2048个字。如今,硬件既便宜又充足,可以提供这样的保证。
答案 1 :(得分:2)
Hans Passant answer深刻地描述了为什么你应该尝试不在“实时”要求下使用延迟初始化。
但是如果你真的需要“懒惰”,你应该尽量不给类用户和实现者施加重复if(!is_constructed) construct();
的负担。
首先,考虑廉价的默认构造,如std::vector
:
vector<int> x;
它构造空向量。并且,例如,您可以安全地调用begin(x)
和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();
}
答案 2 :(得分:1)
如果我们有2个实体,那么两阶段方法的主要“亲”。第一个提供接口IFirst并需要外部ISecond实现。第二个提供ISecond并依次要求IFirst。没有两阶段初始化,这就是“鸡蛋和鸡蛋”无法解决的问题。
根据重物和有限范围(如实时/移动/嵌入式),将物体制作为薄和物可能是值得的。懒惰尽可能。潜在地,在使用某些功能之前提供一系列init调用可能是调用者的责任,只是为了确保在跳板之前所有内容都是正确初始化的。