RAII

  RAII 它的全称应该是“Resource Acquire Is Initial”。这是C++创始人Bjarne Stroustrup发明的词汇,比较令人费解。说起来,RAII的含义倒也不算复杂。用白话说就是:在类的构造函数中分配资源,在析构函数中释放资源。这样,当一个对象创建的时候,构造函数会自动地被调用;而当这个对象被释放的时候,析构函数也会被自动调用。于是乎,一个对象的生命期结束后将会不再占用资源,资源的使用是安全可靠的。
  下面便是在C++中实现RAII的典型代码:
  class file
  {
  public:
  file(string const& name) {
  m_fileHandle=open_file(name.cstr());
  }
  ~file() {
  close_file(m_fileHandle);
  }
  ...
  private:
  handle m_fileHandle;
  }
  很典型的“在构造函数里获取,在析构函数里释放”。如果我写下代码:  
  void fun1() ...{
  file myfile("my.txt");
  ... //操作文件
  } //此处销毁对象,调用析构函数,释放资源
  当函数结束时,局部对象myfile的生命周期也结束了,析构函数便会被调用,资源会得到释放。而且,如果函数中的代码抛出异常,那么析构函数也会被调用,资源同样会得到释放。所以,在RAII下,不仅仅资源安全,也是异常安全的。
  但是,在如下的代码中,资源不是安全的,尽管我们实现了RAII:
  void fun2() ...{
  file pfile=new file("my.txt");
  ... //操作文件
  }
  因为我们在堆上创建了一个对象(通过new),但是却没有释放它。我们必须运用delete操作符显式地加以释放:
  void fun3() ...{
  file pfile=new file("my.txt");
  ... //操作文件
  delete pfile;
  }
  否则,非但对象中的资源得不到释放,连对象本身的内存也得不到回收。(将来,C++的标准中将会引入GC(垃圾收集),但正如下面分析的那样,GC依然无法确保资源的安全)。
  现在,在fun3(),资源是安全的,但却不是异常安全的。因为一旦函数中抛出异常,那么delete pfile;这句代码将没有机会被执行。C++领域的诸位大牛们告诫我们:如果想要在没有GC的情况下确保资源安全和异常安全,那么请使用智能指针:
  void fun4() ...{
  shared_ptr<file> spfile(new file("my.txt"));
  ... //操作文件
  } //此处,spfile结束生命周期的时候,会释放(delete)对象
  那么,智能指针又是怎么做到的呢?下面的代码告诉你其中的把戏(关于智能指针的更进一步的内容,请参考std::auto_ptr,boost或tr1的智能指针):
  template<typename T>
  class smart_ptr
  ...{
  public:
  smart_ptr(T* p):m_ptr(p) ...{}
  ~smart_ptr() ...{ delete m_ptr; }
  ...
  private:
  T* m_ptr;
  }
  没错,还是RAII。也就是说,智能指针通过RAII来确保内存资源的安全,也间接地使得对象上的RAII得到实施。不过,这里的RAII并不是十分严格:对象(所占的内存也是资源)的创建(资源获取)是在构造函数之外进行的。广义上,我们也把它划归RAII范畴。但是,Matthew Wilson在《Imperfect C++》一书中,将其独立出来,称其为RRID(Resource Release Is Destruction)。RRID的实施需要在类的开发者和使用者之间建立契约,采用相同的方法获取和释放资源。比如,如果在shared_ptr构造时使用malloc(),便会出现问题,因为shared_ptr是通过delete释放对象的。
  对于内置了GC的语言,资源管理相对简单。不过,事情并非总是这样。下面的C#代码摘自MSDN Library的C#编程指南,我略微改造了一下:
  static void CodeWithoutCleanup()
  ...{
  System.IO.FileStream file = null;
  System.IO.FileInfo fileInfo = new System.IO.FileInfo("C:\file.txt");
  file = fileInfo.OpenWrite();
  file.WriteByte(0xF);
  }
  那么资源会不会泄漏呢?这取决于对象的实现。如果通过OpenWrite()获得的FileStream对象,在析构函数中执行了文件的释放操作,那么资源最终不会泄露。因为GC最终在执行GC操作的时候,会调用Finalize()函数(C#类的析构函数会隐式地转换成Finalize()函数的重载)。这是由于C#使用了引用语义(严格地讲,是对引用类型使用引用语义),一个对象实际上不是对象本身,而是对象的引用。如同C++中的那样,引用在离开作用域时,是不会释放对象的。否则,便无法将一个对象直接传递到函数之外。在这种情况下,如果没有显式地调用Close()之类的操作,资源将不会得到立刻释放。但是像文件、锁、数据库链接之类属于重要或稀缺的资源,如果等到GC执行回收,会造成资源不足。更有甚者,会造成代码执行上的问题。我曾经遇到过这样一件事:我执行了一个sql操作,获得一个结果集,然后执行下一个sql,结果无法执行。这是因为我使用的SQL Server 2000不允许在一个数据连接上同时打开两个结果集(很多数据库引擎都是这样)。第一个结果集用完后没有立刻释放,而GC操作则尚未启动,于是便造成在一个未关闭结果集的数据连接上无法执行新的sql的问题。
  所以,只要涉及了内存以外的资源,应当尽快释放。(当然,如果内存能够尽快释放,就更好了)。对于上述CodeWithoutCleanup()函数,应当在最后调用file对象上的Close()函数,以便释放文件:
  static void CodeWithoutCleanup()
  ...{
  System.IO.FileStream file = null;
  System.IO.FileInfo fileInfo = new System.IO.FileInfo("C:\file.txt");
  file = fileInfo.OpenWrite();
  file.WriteByte(0xF);
  file.Close();
  }
  现在,这个函数是严格资源安全的,但却不是严格异常安全的。如果在文件的操作中抛出异常,Close()成员将得不到调用。此时,文件也将无法及时关闭,直到GC完成。为此,需要对异常作出处理:
  static void CodeWithCleanup()
  ...{
  System.IO.FileStream file = null;
  System.IO.FileInfo fileInfo = null;
  try
  ...{
  fileInfo = new System.IO.FileInfo("C:\file.txt");
  file = fileInfo.OpenWrite();
  file.WriteByte(0xF);
  }
  catch(System.Exception e)
  ...{
  System.Console.WriteLine(e.Message);
  }
  finally
  ...{
  if (file != null)
  ...{
  file.Close();
  }
  }
  }
  try-catch-finally是处理这种情况的标准语句。但是,相比前面的C++代码fun1()和fun4()繁琐很多。这都是没有RAII的后果啊。下面,我们就来看看,如何在C#整出RAII来。
  一个有效的RAII应当包含两个部分:构造/析构函数的资源获取/释放和确定性的析构函数调用。前者在C#中不成问题,C#有构造函数和析构函数。不过, C#的构造函数和析构函数是不能用于RAII的,原因一会儿会看到。正确的做法是让一个类实现IDisposable接口,在IDisposable:: Dispose()函数中释放资源:
  class RAIIFile : IDisposable
  ...{
  public RAIIFile(string fn) ...{
  System.IO.FileInfo fileInfo = new System.IO.FileInfo(fn);
  file = fileInfo.OpenWrite();
  }

  public void Dispose() ...{
  file.Close();
  }

  private System.IO.FileStream file = null;
  }
  下一步,需要确保文件在退出作用域,或发生异常时被确定性地释放。这项工作需要通过C#的using语句实现:
  static void CodeWithRAII()
  ...{
  using(RAIIFile file=new RAIIFile("C:\file.txt"))
  ...{
  ... //操作文件
  } //文件释放
  }
一旦离开using的作用域,file.Dispose()将被调用,文件便会得到释放,即便抛出异常,亦是如此。相比CodeWithCleanup ()中那坨杂乱繁复的代码,CodeWithRAII()简直可以算作赏心悦目。更重要的是,代码的简洁和规则将会大幅减少出错可能性。值得注意的是 using语句只能作用于实现IDisposable接口的类,即便实现了析构函数也不行。所以对于需要得到RAII的类,必须实现 IDisposable。通常,凡是涉及到资源的类,都应该实现这个接口,便于日后使用。实际上,.net库中的很多与非内存资源有关的类,都实现了 IDisposable,都可以利用using直接实现RAII。
  但是,还有一个问题是using无法解决的,就是如何维持类的成员函数的RAII。我们希望一个类的成员对象在该类实例创建的时候获取资源,而在其销毁的时候释放资源:
  class X
  ...{
  public:
  X():m_file("c:\file.txt") ...{}
  private:
  File m_file; //在X的实例析构时调用File::~File(),释放资源。
  }
  但是在C#中无法实现。由于uing中实例化的对象在离开using域的时候便释放了,无法在构造函数中使用:
  class X
  ...{
  public X() ...{
  using(m_file=new RAIIFile("C:\file.txt"))
  ...{
  }//此处m_file便释放了,此后m_file便指向无效资源
  }
  pravite RAIIFile m_file;
  }
  对于成员对象的RAII只能通过在析构函数或Dispose()中手工地释放。我还没有想出更好的办法来。
  至此,RAII的来龙去脉已经说清楚了,在C#里也能从中汲取到充足的养分。但是,这还不是RAII的全部营养,RAII还有更多的扩展用途。在《Imperfect C++》一书中,Matthew Wilson展示了RAII的一种非常重要的应用。为了不落个鹦鹉学舌的名声,这里我给出一个真实遇到的案例,非常简单:我写的程序需要响应一个Grid 控件的CellTextChange事件,执行一些运算。在响应这个事件(执行运算)的过程中,不能再响应同一个事件,直到处理结束。为此,我设置了一个标志,用来控制事件响应:
  class MyForm
  ...{
  public:
  MyForm():is_cacul(false) ...{}
  ...
  void OnCellTextChange(Cell& cell) ...{
  if(is_cacul)
  return;
  is_cacul=true;
  ... //执行计算任务
  is_cacul=false;
  }
  private:
  bool is_cacul;
  };
  但是,这里的代码不是异常安全的。如果在执行计算的过程中抛出异常,那么is_cacul标志将永远是true。此后,即便是正常的 CellTextChange也无法得到正确地响应。同前面遇到的资源问题一样,传统上我们不得不求助于try-catch语句。但是如果我们运用 RAII,则可以使得代码简化到不能简化,安全到不能再安全。我首先做了一个类:
  class BoolScope
  ...{
  public:
  BoolScope(bool& val, bool newVal)
  :m_val(val), m_old(val) ...{
  m_val=newVal;
  }
  ~BoolScope() ...{
  m_val=m_old;
  }

  private:
  bool& m_val;
  bool m_old;
  };
  这个类的作用是所谓“域守卫(scoping)”,构造函数接受两个参数:第一个是一个bool对象的引用,在构造函数中保存在m_val成员里;第二个是新的值,将被赋予传入的那个bool对象。而该对象的原有值,则保存在m_old成员中。析构函数则将m_old的值返还给m_val,也就是那个 bool对象。有了这个类之后,便可以很优雅地获得异常安全:
  class MyForm
  ...{
  public:
  MyForm():is_cacul(false) ...{}
  ...
  void OnCellTextChange(Cell& cell) ...{
  if(is_cacul)
  return;
  BoolScope bs_(is_cacul, true);
  ... //执行计算任务
  }
  private:
  bool is_cacul;
  };
  好啦,任务完成。在bs_创建的时候,is_cacul的值被替换成true,它的旧值保存在bs_对象中。当OnCellTextChange()返回时,bs_对象会被自动析构,析构函数会自动把保存起来的原值重新赋给is_cacul。一切又都回到原先的样子。同样,如果异常抛出,is_cacul 的值也会得到恢复。
  这个BoolScope可以在将来继续使用,分摊下来的开发成本几乎是0。更进一步,可以开发一个通用的Scope模板,用于所有类型,就像《Imperfect C++》里的那样。
  下面,让我们把战场转移到C#,看看C#是如何实现域守卫的。考虑到C#(.net)的对象模型的特点,我们先实现引用类型的域守卫,然后再来看看如何对付值类型。其原因,一会儿会看到。
  我曾经需要向一个grid中填入数据,但是填入的过程中,控件不断的刷新,造成闪烁,也影响性能,除非把控件上的AutoDraw属性设为false。为此,我做了一个域守卫类,在填写操作之前关上AutoDraw,完成或异常抛出时再打开:
  class DrawScope : IDisposable
  ...{
  public DrawScope(Grid g, bool val) ...{
  m_grid=g;
  m_old=g->AutoDraw;
  m_grid->AutoDraw=val;
  }
  public void Dispose() ...{
  g->AutoDraw=m_old;
  }
  private Grid m_grid;
  private bool m_old;
  };
  于是,我便可以如下优雅地处理AutoDraw属性设置问题:
  static void LoadData(Grid g) ...{
  using(DrawScope ds=new DrawScope(g, false))
  ...{
  ... //执行数据装载
  }
  }
  现在,我们回过头,来实现值类型的域守卫。案例还是采用前面的CellTextChange事件。当我试图着手对那个is_cacul执行域守卫时,遇到了不小的麻烦。起初,我写下了这样的代码:
  class BoolScope
  ...{
  private ??? m_val; //此处用什么类型?
  private bool m_old;
  };
  m_val应当是一个指向一个对象的引用,C#是没有C++那些指针和引用的。在C#中,引用类型定义的对象实际上是一个指向对象的引用;而值类型定义的对象实际上是一个对象,或者说“栈对象”,但却没有一种指向值类型的引用。(关于这种对象模型的优劣,后面的“题外话”小节有一些探讨)。我尝试着采用两种办法,一种不成功,而另一种成功了。
  C#(.net)有一种box机制,可以将一个值对象打包,放到堆中创建。这样,或许可以把一个值对象编程引用对象,构成C#可以引用的东西:
  class BoolScope : IDisposable
  ...{
  public BoolScope(object val, bool newVal) ...{
  m_val=val; //#1
  m_old=(bool)val;
  (bool)m_val=newVal; //#2
  }
  public void Dispose() ...{
  (bool)m_val=m_old; //#3
  }
  private object m_val;
  private bool m_old;
  }
  使用时,应当采用如下形式:
  class MyForm
  ...{
  public MyForm() ...{
  is_cacul=new bool(false); //boxing
  }
  ...
  void OnCellTextChange(Cell& cell) ...{
  if(is_cacul)
  return;
  using(BoolScope bs=new BoolScope(is_cacul, true))
  ...{
  ... //执行计算任务
  }
  }
  private object is_cacul;
  };
  很可惜,此路不通。因为在代码#1的地方,并未执行引用语义,而执行了值语义。也就是说,没有把val(它是个引用)的值赋给m_val(也是个引用),而是为m_val做了个副本。以至于在代码#2和#3处无法将newVal和m_old赋予val(也就是is_cacul)。或许C#的设计者有无数理由说明这种设计的合理性,但是在这里,却扼杀了一个非常有用的idom。而且,缺少对值对象的引用手段,大大限制了语言的灵活性和扩展性。
  第二种方法就非常直白了,也绝对不应当出问题,就是使用包装类:
  class BoolVal
  ...{
  public BoolVal(bool v)
  ...{
  m_val=v;
  }
  public bool getVal() ...{
  return m_val;
  }
  public void setVal(bool v) ...{
  m_val=v;
  }
  private bool m_val;
  }
  class BoolScope : IDisposable
  ...{
  public IntScope(BoolVal iv, bool v)
  ...{
  m_old = iv.getVal();
  m_Val = iv;
  m_Val.setVal(v);
  }
  public virtual void Dispose()
  ...{
  m_Val.setVal(m_old);
  }
  private BoolVal m_Val;
  private bool m_old;
  }
这里,我做了一个包装类BoolVal,是个引用类。然后以此为基础,编写了一个BoolScope类。然后,便可以正常使用域守卫:
  class MyForm
  ...{
  public MyForm() ...{
  m_val.setVal(false); //boxing
  }
  ...
  void OnCellTextChange(Cell& cell) ...{
  if(is_cacul)
  return;
  using(BoolScope bs=new BoolScope(m_val, true))
  ...{
  ... //执行计算任务
  }
  }
  private BoolVal m_val;
  };
  好了,一切都很不错。尽管C#的对象模型给我们平添了不少麻烦,使得我多写了不少代码,但是使用域守卫类仍然是一本万利的事情。作为GP fans,我当然也尝试着在C#里做一些泛型,以免去反复开发包装类和域守卫类的苦恼。这些东西,就留给大家做练习吧。:)
  在某些场合下,我们可能会对一些对象做一些操作,完事后在恢复这个对象的原始状态,这也是域守卫类的用武之地。只是守卫一个结构复杂的类,不是一件轻松的工作。最直接的做法是取出所有的成员数据,在结束后再重新复制回去。这当然是繁复的工作,而且效率不高。但是,我们将在下一篇看到,如果运用swap手法,结合复制构造函数,可以很方便地实现这种域守卫。这我们以后再说。
  域守卫作为RAII的一个扩展应用,非常简单,但却极具实用性。如果我们对“资源”这个概念加以推广,把一些值、状态等等内容都纳入资源的范畴,那么域守卫类的使用是顺理成章的事。

题外话:C#的对象模型
  C#的设计理念是简化语言的学习和使用。但是,就前面案例中出现的问题而言,在特定的情况下,特别是需要灵活和扩展的时候,C#往往表现的差强人意。C# 的对象模型实际上是以堆对象和引用语义为核心的。不过,考虑到维持堆对象的巨大开销和性能损失,应用在一些简单的类型上,比如int、float等等,实在得不尝失。为此,C#将这些简单类型直接作为值处理,当然也允许用户定义自己的值类型。值类型拥有值语义。而值类型的本质是栈对象,引用类型则是堆对象。
  这样看起来应该是个不错的折中,但是实际上却造成了不大不小的麻烦。前面的案例已经明确地表现了这种对象模型引发的麻烦。由于C#抛弃值和引用的差异(为了简化语言的学习和使用),那么对于一个引用对象,我们无法用值语义访问它;而对于一个值对象,我们无法用引用语义访问。对于前者,不会引发本质性的问题,因为我们可以使用成员函数来实现值语义。但是对于后者,则是无法逾越的障碍,就像在BoolScope案例中表现的那样。在这种情况下,我们不得不用引用类包装值类型,使得值类型丧失了原有的性能和资源优势。
  更有甚者,C#的对象模型有时会造成语义上的冲突。由于值类型使用值语义,而引用类型使用引用语义。那么同样是对象定义,便有可能使用不同的语义:
  int i, j=10; //值类型
  i=j; //值语义,两个对象复制内容
  i=5; //i==5, j==10
  StringBuilder s1, s2 = new StringBuilder("s2"); //引用类型
  s1 = s2; //引用语义,s1和s2指向同一个对象
  s1.Append(" is s1"); //s1==s2=="s1 is s2"
  同一个形式具有不同语义,往往会造成意想不到的问题。比如,在软件开发的最初时刻,我们认为某个类型是值类型就足够了,还可以获得性能上的好处。但是,随着项目进入后期阶段,发现最初的设计有问题,值类型限制了该类型的某些特性(如不能拥有析构函数,不能引用等等),那么需要把它改成引用类型。于是便引发一大堆麻烦,需要检查所有使用该类型的代码,然后把赋值操作改成复制操作。这肯定不是讨人喜欢的工作。为此,在实际开发中,很少自定义值类型,以免将来自缚手脚。于是,值类型除了语言内置类型和.net库预定义的类型外,成了一件摆设。
  相比之下,传统语言,如Ada、C、C++、Pascal等,区分引用和值的做法尽管需要初学者花更多的精力理解其中的差别,但在使用中则更加妥善和安全。毕竟学习是暂时的,使用则是永远的。


粗略看了一下,添加一些自己的胡话:
1.RAII通常表示初始化即获取资源,同时享有资源的独占性,从而带来的好处有:
 1)类中状态不需要指示空(有时这个是很难实现的)
 2)类中的成员函数调用不必进行繁琐的 数据是否为空 是否合法 是否完整的检查,从而提高了效率
 3)类中的成员函数调用不必指定一个多余的异常返回,通常这是不优雅,繁琐,而且低效的
 4)合乎C++的思维,Meyers的书里说过,不同于C,C++在“信息完整”时才定义(初始化一个量),这样正是RAII与其配合的完美例子,不但快了效率,而且增加了安全性以及逻辑层次
 5)合理控制了资源,优雅地,透明地,省去了人工配对的重复劳动
 6)容斥性,例如端口,一旦有一个实例拥有了端口,另一个即不允许
2.RAII中所谓的资源通常包括:操作句柄,端口,内存,抽象对象等
3.异常安全,LZ说得够了,我一般很懒,不常用


================================
这个部分Matthew Wilson在《imperfect C++》里有类似的案例。做法是做一个scoping类,在构造函数里调用CoInitilize(),析构函数里调用Uninitilze()。然后用这个scoping类定义一个全局对象。全局对象在程序启动时构造(构造函数调用),在程序结束时销毁(析构函数调用)。这样就用RAII解决了com的Initialize问题。 这里我又想起一个例子,就是M$的COM(我初学,如果有误请指出) 
在使用COM服务前必须要初始化即CoInitilize结束要Uninit 
可怕的是每个COM服务函数(返回HRESULT)几乎都要检查COM是否已初始化,这不是极其低效,烦恼,不优雅的吗? 

当然COM这也是不得已,因为作为API调用的COM,要支持多种语言,RAII对其来说几乎不可能 
================================ 
这个部分Matthew Wilson在《imperfect C++》里有类似的案例。做法是做一个scoping类,在构造函数里调用CoInitilize(),析构函数里调用Uninitilze()。然后用这个scoping类定义一个全局对象。全局对象在程序启动时构造(构造函数调用),在程序结束时销毁(析构函数调用)。这样就用RAII解决了com的Initialize问题。
--------------------------------


从语义上说,构造函数和析构函数用于处理对象在创建时和销毁时应该做的事,从而保证了在(且仅在)一定的时间和空间范围(或称“域”)内对象的有效性。而对于资源常常需要同样的保证。这就使的RAII成为可能,即使用对象的基本设施去实现对资源的管理,只因它们符合同样的客观规律(理由)。简单地说,OO的合理性促成了RAII的合理性。不仅如此,对于其他遵循此项规律的特性,都可以使用这个方法。比如除了资源安全外,还有异常安全和线程安全,等等。


这个例子暗示了一条经验:尽可能多地保持设计的合理性。因为,如果设计更合理,那么它不仅对现在有用,还可能对将来有用。



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
互斥锁(mutex)RAII是一种使用资源获取即初始化(Resource Acquisition Is Initialization,RAII)的技术来管理互斥锁的方式。RAII是一种C++编程范式,它利用对象的构造函数和析构函数来自动管理资源的获取和释放。 在使用互斥锁时,我们需要在临界区代码之前获取锁,在临界区代码之后释放锁,以确保多个线程之间的同步。RAII技术可以帮助我们避免手动管理锁的获取和释放过程,从而减少错误和资源泄漏的可能性。 使用互斥锁RAII的基本思想是创建一个类,将互斥锁作为类成员,并在类的构造函数中获取锁,在析构函数中释放锁。这样,当类对象作用域结束时,析构函数会自动被调用,从而释放锁资源。 下面是一个简单的示例代码,演示了如何使用互斥锁RAII: ```cpp #include <mutex> class MutexRAII { public: MutexRAII(std::mutex& mtx) : mutex(mtx) { mutex.lock(); } ~MutexRAII() { mutex.unlock(); } private: std::mutex& mutex; }; // 使用示例 std::mutex mtx; void criticalSection() { MutexRAII lock(mtx); // 在临界区前获取锁 // 执行临界区代码 // ... } // 在作用域结束时自动释放锁 ``` 在这个示例中,MutexRAII类将互斥锁作为成员变量,并在构造函数中获取锁,析构函数中释放锁。在criticalSection函数中,创建MutexRAII对象lock时会自动获取锁,当lock对象的作用域结束时,析构函数会自动被调用,从而释放锁。这样,我们就实现了互斥锁的RAII管理。 通过使用互斥锁RAII,我们可以更方便地管理互斥锁资源,避免手动操作锁,提高代码的可性和可维护性。同时,RAII也能够在异常情况下正确地释放锁资源,避免资源泄漏的问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值