`
zhc0822
  • 浏览: 228959 次
  • 性别: Icon_minigender_1
  • 来自: 宝仔的奇幻城堡
社区版块
存档分类
最新评论

内存受限下的设计模式(3)——局部毁损

阅读更多
背景
A、B两公司主营相同的业务。A公司耗费两个月时间,开发出一个基于flash的RIA站点用于宣传。B公司耗费一周,开发了一个简单的CMS用于宣传。顾客Z的浏览器不支持flash或禁用了flash。Z先访问了A公司的站点,然后再访问B公司的站点。请问哪家公司的胜算更大些?或者说,哪家公司在竞争中占得了先机?
C公司也耗费两个月时间开发了一个基于JavaScript的RIA站点用于宣传,C公司还多花费了半个月的时间为不支持或禁用了JavaScript的浏览器开发了一个降级版本的站点。
现在,C至少在网络宣传方面将立于不败之地。
Web开发领域中将这种模式称为“优雅降级”或“渐进增强”。嵌入式领域将其称为“局部毁损”。
上面这则故事告诉我们至少三个道理:
1、采用局部毁损模式的产品更可靠;
2、采用局部毁损模式的产品要求更有经验的开发人员和更长的开发周期;
3、搞前端的人可能是和美术接触的比较多,起个名字都这么浪漫——优雅降级;而搞嵌入式显然要更死板更学究气一点——局部毁损。

模式
局部毁损模式要求我们,即便在系统内存耗尽的时候,也要让系统处于安全的状态。

更深入的探讨
局部毁损蕴含着三处值得思考的问题:

Q1:如何确定毁损的时机?
答:当内存耗尽时。一般来说,内存耗尽的消息是直接通过编程语言获知的。C语言通常返回一个错误码来暗示内存已耗尽;Java和C++一般通过异常的方式来通知内存已经耗尽。

Q2:毁损哪一个或多个组件?
答:毁损的一定不是关键的部件。比如我现在在使用javaeye的文本编辑器输入我的博文,则至少应当保证我能输入基本的文字,诸如表情、字体等功能,可以酌情毁损。

Q3:毁损的方式是什么?
答:拒绝申请内存的操作。

Q4:组件如何响应毁损动作?
答:一旦组件侦测到内存分配失败,它必须有能力回复到上一个安全状态,消除因为分配失败所导致的矛盾。有两种策略,适用的对象不同:
1、撤回,即简单地忽略这次内存请求。适用于后台运行的组件。(见下图)




2、降级,即删减不是那么重要又很耗费内存的功能,为关键功能腾出内存空间。适用于用户可见的组件(如UI)。(见下图)



实现
第一步:侦测内存耗尽。
如果从堆中分配内存,则编程语言本身应当会提供某种提醒分配失败的机制。如果使用固定结构分配,那么程序员自己就要保证该组件有能力检查并确定所使用的固定结构已满。

第二步:考虑受影响的组件。
一旦发现内存耗尽,必须确定一个系统中究竟有多少组件受到了这个错误的影响。OO的特性能够减少错误传播的趋势,但是就像我在《内存受限下的设计模式(2)——小型接口》中叙述的一样,OO在嵌入式领域只是一种选择,但绝非最佳选择。

第三步:释放资源。
在“撤回”(第一幅图)中,为了保证不留下任何副作用,凡是受到错误影响的组件,都要释放已经分配但无法使用的内存,并将其状态恢复至错误发生以前的状态。
C++的异常在try和catch语句之间通常要进行stack unwind。此时C++ run time会调用所有在try块中构造的对象的析构函数。调用的顺序和它们在try中构造的顺序相反。让我们来看下面这个例子(XL C/C++ V8.0 for AIX):
#include <iostream>
using namespace std;

struct E {
  const char* message;
  E(const char* arg) : message(arg) { }
};

void my_terminate() {
  cout << "Call to my_terminate" << endl;
};

struct A {
  A() { cout << "In constructor of A" << endl; }
  ~A() {
    cout << "In destructor of A" << endl;
    throw E("Exception thrown in ~A()");
  }
};

struct B {
  B() { cout << "In constructor of B" << endl; }
  ~B() { cout << "In destructor of B" << endl; }
};

int main() {
  set_terminate(my_terminate);

  try {
    cout << "In try block" << endl;
    A a;
    B b;
    throw("Exception thrown in try block of main()");
  }
  catch (const char* e) {
    cout << "Exception: " << e << endl;
  }
  catch (...) {
    cout << "Some exception caught in main()" << endl;
  }

  cout << "Resume execution of main()" << endl;
}

输出的结果是:
引用
In try block
In constructor of A
In constructor of B
In destructor of B
In destructor of A
Call to my_terminate


java有自己的垃圾回收机制来释放资源。我们在finally块中书写撤回的代码,从而在清理结束之后恢复至上一个正常状态。请看下面的代码片段:
Command cmd = new Command();
commands.add(cmd);
try{
     cmd.execute();
}
finally{
     // 恢复至上一个正确状态
     commands.remove(cmd);
}

在Symbian中,局部毁损是Symbian基本体系结构的原则之一。Symbian C++不使用标准C++的异常,改用自身提供的TRAP模型。

第四步:降级执行
第三步完成之后,我们可能仍然要确保程序继续执行。“撤回”的做法可以直接跳过此步骤。但“降级”的做法必须在这个步骤中多做一些文章。
比如:
  • vista操作系统会在内存不足时自动关闭aero特效(当年1G内存跑vista经常会遇到这个状况)
  • firefox浏览器会在页面脚本耗尽资源时弹出对话框提示是否停止执行本页的脚本(你可以自己写一个js的死循环试一下)。

第五步:未雨绸缪。
其实这一步应当算作是第零步才对。
vista在关闭aero开启普通效果时,必须保留足够的内存供普通效果使用。
让我们深入语言细节。即便是异常处理,也需要额外的内存。
因此我们必须保留一些内存以备不测。

示例
  • 使用“撤回”策略:

Command cmd = new Command();
commands.add(cmd);
try{
     cmd.execute();
}
finally{
     // 恢复至上一个正确状态
     commands.remove(cmd);
}

  • 使用“降级”策略:

class Image{
     // 创建一个中等画质和小尺寸的图片作为默认图片
     static Picture defaultPic = new Picture(Picture.MEDIUM_QUALITY, Picture.SMALL_SIZE);

     // 返回默认图片
     public static defaultPicture(){
          return defaultPic;
     }

     // 创建图片
     public static Picture createPic(int quality, int size){
          Picture pic;
          try{
               pic = new Picture(quality, size);
          } 
          catch(OutOfMemoryException e){
               // 内存满,返回默认图片
               return defaultPicture();
          }
     }
}

调用Image.createPic创建高画质大尺寸图片时,即便内存分配失败,也至少可以保证返回一个中等画质和小尺寸的图片。注意如果连defaultPic的内存都分配失败,则该组件根本无法启动。

最后,再次提醒一句:局部毁损模式要求富有经验的程序员,他应当至少知晓:1、何处有毁损风险;2、使用“撤回”还是使用“降级”来实现毁损。

预告
下一篇,介绍欧茨队长。
  • 大小: 8.1 KB
  • 大小: 9.7 KB
1
0
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics