C++ 全有或全无的类似事务的模式

C++ Transaction-like Pattern for All or Nothing Work

本文关键字:模式 事务 C++      更新时间:2023-10-16

假设我有两个函数DoTaskADoTaskB——都能够抛出TaskException——以及它们对应的"回滚"函数UndoTaskAUndoTaskB。 使用哪种最佳模式才能使两者都成功或两者都失败?

我现在拥有的最好的是

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 中有一些我不知道的东西,可以更好地编码它?

一点 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;
    }
}

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


.PPS。原则上,您可以在 RAII dtor 中使用std::uncaught_exception,而不是显式提交。 我更喜欢在这里明确提交,因为我认为它更清晰,并且如果您以return FAILURE_CODE而不是异常提前退出范围,也可以正常工作。

很难

在C++中实现事务一致性。在Dr. Dobb的日记中,有一个很好的方法使用ScopeGuard模式进行了描述。这种方法的优点在于,这在正常情况和异常情况下都需要清理。它利用了这样一个事实,即确保对象析构函数调用任何范围出口,而异常情况只是另一个范围出口。

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

你封装了执行DoTaskA()操作所需的所有数据在命令类的对象中,具有奖励,您可以反转所有这些都,如果需要(因此,如果失败,则无需进行特殊撤消执行)。命令模式特别适合处理"全有或全无"情况。

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

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

实现此目的的最佳方法是使用作用域防护,基本上是一个小型 RAII 习惯用法,如果引发异常,它将调用回滚处理程序。

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

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

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

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

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