受检异常 非受检异常_C++异常实战之十一 使用scope_fail处理复杂场景(非fail-fast)下的异常...

本章引入一个额外的机制scope_exit/scope_fail,以处理复杂场景下的cleanup与rollback,同时,对RAII语义进行一定语义上的修正,以提供一个更加友好的异常处理的心智模型。

前面介绍了Exception safety的概念,而实际上,在复杂场景下(一个函数分配了大量非内存资源、产生了大量的非资源分配的副作用),实现Exception safety并不总是那么简单。


一个实际的异常处理例子

考虑以下问题,假设我有一个函数,它由若干个步骤组成,每个步骤都会产生副作用,且每个步骤都可能失败,并抛出异常。

void foo()
{
    doA();
    doB();
    doC();
    doD();
}

上面几行代码很干净,但是你能判断它有bug吗?

假设doB失败了,我们实际上是需要回滚doA的,因为它产生了副作用,这个副作用可能是分配了一个对象,也可能是其他的,比如创建了一台EC2、订了一张机票。是的,我们需要回滚,于是,代码变成了这样:

void foo()
{
    doA();

    try {
        doB();
    } catch (...) {
        rollbackA();

        throw;
    }

    try {
        doC();
    } catch (...) {
        rollbackB();
        rollbackA();
        
        throw;
    }

    try {
        doD();
    } catch (...) {
        rollbackC();
        rollbackB();
        rollbackA();

        throw;
    }
}

更进一步的,假如这些rollbackX也会抛出异常,怎么办?代码怎么写?当然,这个更多的是设计问题。

  • 设计一:清理函数绝对不对抛出异常。
    • 对于简单内存资源,这个是成立的。
    • 但是,如果我需要释放一台我刚刚买的EC2,但是AWS的API也异常了呢?
  • 设计二:抛出异常的话,直接打日志然后忽略吧。我们的系统会保证最终一致。
    • 如果是这样的话,我还处理干什么?直接依赖最终一致就好啦。
    • 比如:doA是分配EC2,doD是将分配的EC2 id写入数据库,我们的系统后台会定时repair,当发现账号下某台EC2不在数据库中,并且已经持续1小时了,那就释放它。
  • 设计三:抛出异常的话,就把抛出的异常向上抛。
    • 啊。。。要不看看设计四
  • 设计四:抛出异常的话,先记下,依然把所有的rollback函数都执行了,然后?
    • rollbackC失败了,rollbackB/rollbackA还执行吗?
    • 如果尽力把所有rollback都执行了话,假设三个rollback,抛出了两个异常,我上报哪个?还是直接上报root cause(doA/B/C/D失败所抛出的异常)?

对于设计一,用C++11可以这么写:

void foo()
{
    doA();
    // the construction should never throw
    Scoped_guard defer1 = []{
        rollbackA();
    };

    doB();
    // the construction should never throw
    Scoped_guard defer2 = []{
        rollbackB();
    };

    doC();
    // the construction should never throw
    Scoped_guard defer3 = []{
        rollbackC();
    };

    doD();

    // critical line
    defer1.release();
    defer2.release();
    defer3.release();
}

其中,Scoped_guard是一个RAII对象,析构时执行某个函数。调用release的话,析构时什么都不做。

C++17对这种情况下进行了优化:

void foo()
{
    doA();
    scope_fail defer1([]{
        rollbackA();
    });

    doB();
    // the construction should never throw
    scope_fail defer2([]{
        rollbackB();
    });

    doC();
    // the construction should never throw
    scope_fail defer3([]{
        rollbackC();
    });

    doD();
}

scope_fail会在函数异常退出时执行,函数正常退出不会执行。

对于设计二也可以使用类似的方式执行,只需要在scope_fail中手动try/catch一下,并写条日志,确保rollbackX的异常不会继续向外扩散。

看起来也不那么差嘛。唯一的问题是,我依赖RAII来处理异常流,这与RAII是用来管理资源的本质有些冲突,并且,也继承了析构函数无法抛出异常的问题。这就意味着,如果我想使用设计三,异常+RAII无法优雅地完成,只能用最开始那种一个语句一个try/catch的做法(这种做法和错误码比就没啥区别了)。

如果你想选择设计四,em...取决于在rollback异常情况下,你到底想抛出哪个异常,设计四可以化归为设计二或者设计三。

额外提一句,如果rollback会失败的话,系统肯定需要一个后台repair线程去维护系统一致性,或者报警后人工介入。如果是repair线程来处理的话,这么写可能更加清晰些:

void foo()
try {
    doA();
    doB();
    doC();
    doD();
} catch (...) {
    tryRepair();

    throw;
}

啥,你还想更简单?

void foo()
{
    scope_fail defer(tryRepair);
    doA();
    doB();
    doC();
    doD();
}

em...用RAII来充当控制流、异常流,真的不违合吗?

最后,给下错误码的写法,大家觉得哪种更容易读、更容易写、更容易review?

int foo()
{
    int r = doA();
    if (r) {
        goto out;
    }

    r = doB();
    if (r) {
        goto rollback1;
    }

    r = doC();
    if (r) {
        goto rollback2;
    }

    r = doD();
    if (r) {
        goto rollback3;
    }

    goto out;

rollback3:
    rollbackC();
rollback2:
    rollbackB();
rollback1:
    rollbackA();
out:
    return r;
}

R(esponsibility) acquisition is initialization

将RAII中的R单纯理解为Resource对实现异常安全并不够,我们需要将R理解为Responsibility,即一个职责,我们不仅仅而一个资源,而是管理一个职责。

职责即在所有路径上都必须被调用的函数。

  • 每个职责由一个对象管理
  • 每个对象管理一个职责
  • 一旦某个职责需要每调用,立即构造对象来管理它,不要手动管理职

典型的,cleanup与rollback,都是职责。如果看到手动调用cleanup函数或者rollback函数,就要小心了。


异常处理的心智模型

  • 控制流 for 业务流(根据业务执行的具体情况,可能会进不同分支)
  • 异常处理 for 异常流(中止业务流)
  • RAII for 强制执行的流(无论业务流怎么跑,业务流是否被异常中止,都要执行的职责)
    • 清理
      • 清理资源 => scope_exit or 资源对象
      • 清理做了一半的任务,i.e. rollback => scope_fail
      • scope_success目前未看到明确的语义,不建议使用
    • 其他

总结

  • 错误处理非常非常非常复杂,非常非常非常容易隐藏bug,一个系统的、清晰的错误处理机制与设计,对降低复杂度有着至关重要的意义。
  • 对错误处理策略的设计很重要:
    • rollback是否允许出错?
    • rollback出错后怎么办?
    • rollback出错后,是上报rollback的错误,还是继续上报之前的错误?
    • 是否需要后台repair?
    • 是否需要人工干预?
  • C++异常的确可以写出更加干净的代码,但它的学习负担、心智负担,都非常高,慎用。
  • 错误码保平安。

参考文章

Programming in Lua(二)- 异常与错误码​techsingular.net
d5fb833a5e01599bc5effbe6abc1f219.png
Cleaner, more elegant, and wrong | The Old New Thing​devblogs.microsoft.com
9f3a7e34de09c931eafa269bd4911c8f.png
Cleaner, more elegant, and harder to recognize | The Old New Thing​devblogs.microsoft.com
9f3a7e34de09c931eafa269bd4911c8f.png
uncaught_exceptions and ScopeGuard​www.open-std.org Write Exception Safe Code​exceptionsafecode.com scope_exit/scope_fail​en.cppreference.com
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值