10

2つの関数が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は不要ですが、後で3番目または4番目のタスクを追加する場合に備えて、コードの対称性を示すのがよいでしょう。

補助ブール変数があるため、このコードは気に入らない。おそらく、新しい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オブジェクトが多数ある場合は、それらすべてをコンテナーにプッシュして、コンテナーに同じセマンティクスを与えることをお勧めします(コンテナーでcommitを呼び出して、所有する各ガードオブジェクトをコミットします)。


PPS。std::uncaught_exception原則として、明示的にコミットする代わりに、RAIIdtorで使用できます。return FAILURE_CODEここで明示的にコミットすることをお勧めします。これは、より明確であり、例外ではなく早期にスコープを終了した場合にも正しく機能すると思うためです。

于 2012-06-11T16:38:46.430 に答える
7

C++でトランザクションの一貫性を実現することは困難です。Dr Dobbのジャーナルには、 ScopeGuardパターンを使用して説明されている優れた方法があります。このアプローチの利点は、通常の状況と例外シナリオの両方でクリーンアップが必要になることです。これは、オブジェクトデストラクタがスコープ出口を呼び出すことが保証されているという事実を利用しており、例外ケースは単なる別のスコープ出口です。

于 2012-06-11T16:25:47.793 に答える
1

CommandPatternについて考えたことはありますか?コマンドパターンの説明

DoTaskA()が実行することを実行するために必要なすべてのデータをコマンドクラスのオブジェクトにカプセル化し、必要に応じてこれらすべてを元に戻すことができるというボーナスがあります(したがって、実行に失敗した場合に特別な取り消しを行う必要はありません) 。コマンドパターンは、「オールオアナッシング」の状況を処理するのに特に適しています。

例を読むことができるように、相互に構築する複数のコマンドがある場合は、責任の連鎖を調査する必要があります

おそらくリアクターパターンが役立つかもしれません(ここでのリアクターの説明)これは制御の流れを逆転させますが、それは自然に感じられ、システムを強力なマルチスレッド、マルチコンポーネント設計に変えるという利点があります。しかし、ここではやり過ぎかもしれません。例からはわかりません。

于 2012-06-11T16:20:08.547 に答える
1

これを実現する最善の方法は、スコープガードを使用することです。これは、基本的に、例外がスローされた場合にロールバックハンドラーを呼び出す小さなRAIIイディオムです。

少し前にScopeGuardの簡単な実装について質問しましたが、その質問は、実稼働プロジェクトで使用している優れた実装に発展しました。ロールバックハンドラーとしてc++11とラムダで動作します。

私のソースには実際には2つのバージョンがあります。1つはコンストラクターハンドラーがスローした場合にロールバックハンドラーを呼び出すもので、もう1つはそれが発生した場合にスローしないものです。

ここでソースと使用例を確認してください。

于 2012-06-11T16:54:42.610 に答える
0

スケーラビリティーのために、コンテナー内のタスクに対して元に戻す必要があるという事実を保存する必要があります。次に、catchブロックで、コンテナに記録されているすべての取り消しを呼び出します。

たとえば、コンテナには、正常に完了したタスクを元に戻すための関数オブジェクトを含めることができます。

于 2012-06-11T16:19:38.493 に答える