假设我有两个函数DoTaskA
和DoTaskB
- 都能投掷TaskException
- 以及相应的“回滚”函数UndoTaskA
和UndoTaskB
。什么是最好的模式,以便两者都成功或两者都失败?
我现在最好的是
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中有一些我不知道的东西,可以更好地编写代码吗?
答案 0 :(得分: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();
}
};
因此,我们假设在事务成功后我们将始终创建保护对象,并且只有在 all 事务成功后才调用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 对象推送到容器中,如果你有很多,给容器提供相同的语义(在容器上调用commit)让它提交每个拥有的守卫对象。)
PPS。原则上,您可以在RAII dtor中使用std::uncaught_exception
而不是显式提交。我更喜欢在这里明确提交,因为我认为它更清晰,并且如果您使用return FAILURE_CODE
提前退出范围而不是例外,也可以正常工作。
答案 1 :(得分:7)
很难在C ++中实现事务一致性。在Dobb博士的期刊中使用ScopeGuard模式描述了一种很好的方法。该方法的优点在于它在正常情况和异常情况下都需要清理。它利用了以下事实:确保对象析构函数调用任何作用域出口,而异常情况只是另一个作用域出口。
答案 2 :(得分:1)
你有没有想过CommandPattern? Command Pattern description
您封装了执行DoTaskA()所需的所有数据 在具有奖金的命令类的对象中,您可以反转 所有这些,如果需要的话(因此如果失败则不需要特殊的撤销 执行)。命令模式特别适合处理“全有或全无” 的情况。
如果你有多个相互构建的命令,那就作为你的例子 可以阅读,然后你应该调查chain of responsibility
或许反应堆模式可能派上用场(reactor description here) 这将颠倒控制流,但它感觉自然而且有 将您的系统转变为强大的多线程,多组件的好处 设计。但这里可能有些过分,很难从这个例子中看出来。
答案 3 :(得分:1)
实现这一目标的最佳方法是使用范围保护,基本上是一个小的RAII习惯用法,如果抛出异常,它将调用回滚处理程序。
我已经问过一个简单的ScopeGuard实现了,问题演变成了我在生产项目中使用的一个很好的实现。它适用于c ++ 11和lambdas作为回滚处理程序。
我的源实际上有两个版本:一个将在构造函数处理程序抛出时调用回滚处理程序,另一个在发生这种情况时不会抛出。
检查来源和使用示例in here。
答案 4 :(得分:0)
对于可伸缩性,您希望保存需要对容器中的任务执行撤消的事实。然后,在catch块中,您只需调用容器中记录的所有撤消。
例如,容器可以包含用于撤消已成功完成的任务的函数对象。