10

假设我有两个函数——都能够抛出——DoTaskA以及它们对应的“回滚”函数和. 最好的模式是什么,以使两者都成功或都失败?DoTaskBTaskExceptionUndoTaskAUndoTaskB

我现在最好的是

bool is_task_a_done = false,
     is_task_b_done = false;

try {
    DoTaskA();
    is_task_a_done = true;

    DoTaskB();
    is_task_b_done = true;
} catch (TaskException &e) {
    // Before rethrowing, undo any partial work.
    if (is_task_b_done) {
        UndoTaskB();
    }
    if (is_task_a_done) {
        UndoTaskA();
    }
    throw;
}

我知道这is_task_b_done是不必要的,但如果我们稍后添加第三个或第四个任务,展示代码对称性可能会很好。

由于辅助布尔变量,不喜欢此代码。也许新的 C++11 中有一些我不知道的东西,它可以更好地编码?

4

5 回答 5

13

一个小的 RAII 提交/回滚范围保护可能如下所示:

#include <utility>
#include <functional>

class CommitOrRollback
{
    bool committed;
    std::function<void()> rollback;

public:
    CommitOrRollback(std::function<void()> &&fail_handler)
        : committed(false),
          rollback(std::move(fail_handler))
    {
    }

    void commit() noexcept { committed = true; }

    ~CommitOrRollback()
    {
        if (!committed)
            rollback();
    }
};

所以,我们假设我们总是在事务成功后创建保护对象,并且commit只有在所有事务都成功后才调用。

void complicated_task_a();
void complicated_task_b();

void rollback_a();
void rollback_b();

int main()
{
    try {
        complicated_task_a();
        // if this ^ throws, assume there is nothing to roll back
        // ie, complicated_task_a is internally exception safe
        CommitOrRollback taskA(rollback_a);

        complicated_task_b();
        // if this ^ throws however, taskA will be destroyed and the
        // destructor will invoke rollback_a
        CommitOrRollback taskB(rollback_b);


        // now we're done with everything that could throw, commit all
        taskA.commit();
        taskB.commit();

        // when taskA and taskB go out of scope now, they won't roll back
        return 0;
    } catch(...) {
        return 1;
    }
}

PS。正如Anon Mail所说,如果你有很多 taskX 对象,最好将所有这些taskX对象推送到一个容器中,从而为容器提供相同的语义(在容器上调用 commit 以使其提交每个拥有的保护对象)。


聚苯乙烯。原则上,您可以std::uncaught_exception在 RAII dtor 中使用,而不是显式提交。我更喜欢在此处显式提交,因为我认为它更清晰,并且如果您提前退出范围并使用 areturn FAILURE_CODE而不是异常也可以正常工作。

于 2012-06-11T16:38:46.430 回答
7

在 C++ 中很难实现事务一致性。Dobb 博士的期刊中描述了一种使用ScopeGuard模式的好方法。该方法的美妙之处在于,这需要在正常情况和异常情况下进行清理。它利用了确保对象析构函数在任何范围出口上调用的事实,而异常情况只是另一个范围出口。

于 2012-06-11T16:25:47.793 回答
1

你有没有想过CommandPattern?命令模式说明

您将执行 DoTaskA() 所需的所有数据封装在命令类的对象中,另外,如果需要,您可以反转所有这些(因此,如果执行失败,则无需特殊撤消) . 命令模式特别适合处理“全有或全无”的情况。

如果您有多个相互依赖的命令,如您的示例可以阅读,那么您应该调查责任链

也许反应器模式可能会派上用场(这里的反应器描述)这将反转控制流,但它感觉很自然,并且具有将您的系统变成强大的多线程、多组件设计的好处。但在这里可能有点矫枉过正,很难从例子中看出。

于 2012-06-11T16:20:08.547 回答
1

实现这一点的最佳方法是使用范围保护,基本上是一个小的 RAII 习惯用法,如果抛出异常,它将调用回滚处理程序。

不久前我询问了 ScopeGuard 的简单实现,这个问题演变成我在生产项目中使用的一个很好的实现。它使用 c++11 和 lambdas 作为回滚处理程序。

我的源代码实际上有两个版本:一个在构造函数处理程序抛出时调用回滚处理程序,另一个在发生这种情况时不会抛出。

在此处查看源代码和使用示例。

于 2012-06-11T16:54:42.610 回答
0

对于可伸缩性,您希望保存需要对容器中的任务执行撤消的事实。然后,在 catch 块中,您只需调用容器中记录的所有撤消操作。

例如,容器可以包含功能对象以撤消已成功完成的任务。

于 2012-06-11T16:19:38.493 回答