可重入/线程安全/信号安全/异常安全

本文深入探讨了可重入函数和线程安全的概念,并解释了它们的实现条件。同时,文章阐述了异常安全的不同层次及其实现方法,包括如何在异常处理中避免资源泄漏和数据结构破坏。此外,提供了异常处理的最佳实践,如构造函数和析构函数的使用,以及如何在catch块中正确处理异常。


http://www.ibm.com/developerworks/cn/linux/l-reent.html


1.可重入/线程安全/   

【1】 可重入函数:也就是异步信号安全函数,一般是除了使用自己栈上的变量以外不依赖于任何环境的purecode(纯代码)。这样,它的输出只依赖于输入参数;当输入相同时,输出一定相同。由于与外界没有任何耦合,因此,可以被信号随意中断都没问题。

      【2】线程安全函数,包括可重入函数,以及另一种函数:使用了静态或全局变量,但也使用了锁进行保护。这样,在多线程环境下就不至于出错了。那么后面这种函数为什么不是可重入的呢?貌似被中端打断也没事啊!考虑以下情况:当出现信号中断的,中断函数里再次调用此函数,此时就会出现死锁!

     【3】



可重入的条件:

1、不在函数内部使用静态或全局数据
2、不返回静态或全局数据,所有数据都由函数的调用者提供。
3、使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。
4、不调用不可重入函数。


函数举例:

ctime_r是线程安全的(_r结尾的一般都是线程安全的。)

strtok函数是既不可重入的,也不是线程安全的;加锁的strtok不是可重入的,但线程安全;而strtok_r既是可重入的,也是线程安全的。

函数调用了调用了 mallocfree,则不可重入。malloc 通常会为所有它所分配的区域维持一个链表,而它又可能正在修改那个链表



2.异常安全

一段代码是异常安全的,如果这段代码运行时的失败不会产生有害后果,如内存泄露、存储数据混淆、或无效的输出。异常安全可分成不同层次:

  1. 失败透明(failure transparency), 也称作不抛出保证(no throw guarantee): 代码的运行保证能成功并满足所有的约束条件,即使存在异常情况。如果出现了异常,将不会对外进一步抛出该异常。(异常安全的最好的层次)
  2. 提交或卷回的语义(commit or rollback semantics), 或称作强异常安全(strong exception safety)无变化保证(no-change guarantee): 运行可以是失败,但失败的运行保证不会有负效应,因此所有涉及的数据都保持代码运行前的初始值。[1]
  3. 基本异常安全(basic exception safety): 失败运行的已执行的操作可能引起了副作用,但会保证状态不变。所有存储数据保持有效值,即使这些数据与异常发生前的值有所不同。
  4. 最小异常安全(minimal exception safety也称作无泄漏保证(no-leak guarantee): 失败运行的已执行的操作可能在存储数据中保存了无效的值,但不会引起崩溃,资源不会泄漏。
  5. 异常不安全(no exception safety): 没有保证 (最差的异常安全层次).

例如,考虑一个smart vector类型, 如C++'s std::vector或Java's ArrayList. 当一个数据项x插入vectorv, 必须实际增加x的值到vector的内部对象列表中并且修改vector的计数域以正确表示v中保存了多少数据项;此时如果已有的存储空间不够大,就需要分配新的内存。内存分配可能会失败并抛出异常。因此,vector数据类型如果是“失败透明”保证将会非常困难甚至不可能实现。但vector类型提供“强异常安全”保证却是相当容易的;在这种情况下,x插入v或者成功, 或者v保持不变. 如果vector类型仅提供“基本异常安全”保证, 如果数据插入失败,v可能包含也可能不包含x的值, 但至少v的内部表示是一致的. 但如果vector数据类型是“最小异常安全”保证,v可能会是无效的,例如v的计数域被增加了, 但x并未实际插入, 使得内部状态不一致. 对于“异常不安全”的实现,程序可能会崩溃,例如写入数据到无效的内存。

通常至少需要基本异常安全。失败透明是难于实现的,特别是在编写库函数时,因为对应用程序的复杂知识缺少获知。

当抛出异常时,异常安全的代码应该做到:

l  不 要泄漏资源。

l  不能让数据结构遭到破坏。

第一点可以由以下来方法做到:资源获取即初始化(Resource acquisition is initialization,RAII)是指:为了更为方便、鲁棒地释放已获取的资源,避免资源死锁,一个办法是把资源数据用对象封装起来。程序发生异常,执行栈卷回时,封装了资源的对象会被自动调用其析构函数以释放资源。(也就是说可以把lock啊,内存动态分配包装到类里,指针用智能指针来管理)。

第二点往往使用copy and swap来做到。先赋值给副本,然后再与副本swap。




异常处理总结:

1.构造函数可以抛出异常,但是应该先释放掉已经获取的资源再抛出。更好的做法是,构造函数仅完成简单的、不会抛出异常的工作,将复杂的工作“外包”给别的成员函数。

2.析构函数不应该抛出异常。当然,抛出的话编译是可以过的。但是呢,一般,一个普通的异常发生时,就会去找catch块,然后之前的try块或者函数中的局部对象就被释放()析构了。这时,假如这些对象的析构函数居然又抛出异常,那么前一个异常还没处理,后面又来异常,程序可能就挂掉了。

3.throw e的时候,e拷贝构造为e1,e1不在堆中也不在栈中,在另一个地方。

4.catch(Exception &e)这种写法比较好,用引用,节约资源。不要用传值也不要用传地址。

5.异常规格void func()throw()或者void func()throw(runtime_error, logical_error)表示,他不抛出异常,或者只可能抛出runtime_error或者logical_error。但是这只是一种口头承诺,具体会不会抛出,自然要看具体实现了!如果抛出了不该抛出的异常(违背承诺),那么就会调用unexpected函数。

6.try可以跟多个catch。catch匹配异常对象的类型时,很严格的,没有隐式转换,也没有类型提升,除了:非const到const的转换;子类到父类的转换;数组名或函数名转为对应的指针。

7.C++默认定义了exception,bad_alloc,bad_cast,runtime_error,logical_error等异常。

8.如果函数没有catch,那么就向上throw,一直到main,依然没有处理的话,就调用terminate。

9.catch中,可以再次抛出异常(throw;),而不是(throw e;),前者抛出本身,没有拷贝构造。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值