web-dev-qa-db-ja.com

コールバックの登録とトリガーが無限再帰を引き起こさないことを確認してください

C++/Qtアプリケーションでスタックオーバーフローをデバッグするために、長くて悲惨な1週間を過ごしました。基本的な問題は、コールバックを受け入れる関数があり、特定のケースでは、元の関数が戻る前に(別の関数で)コールバックがトリガーされたことです。この場合、コールバックにより、コールバック自体が完了する前に、同じコールバックが登録されていました。

私がQtイベントループを使用しているため、解決策は、コールバックを直接呼び出すのではなく、シングルショットQTimerによってトリガーされるようにスケジュールすることでした。 (Qtを使用したことがない人にとって、タイムアウトが0ミリ秒のシングルショットQTimerは、作業ユニットがイベントループによってトリガーされることを保証する方法にすぎません。Boost-Asioのio_service::postとほぼ同じです。)または、コールバック自体が直接呼び出すのではなく、コールバック登録関数をスケジュールすることもできます。

これは既知の問題ですか?それを処理する標準的な方法はありますか?

役立つと思われるいくつかの可能なベストプラクティスガイドラインがあります。

  • すべてのコールバックは、有効期間が短い(フラグを設定してから戻るなど)か、プログラムのメインイベントループによって直接トリガーされる「長い」コールバックをスケジュールする必要があります。
    • これには、クライアントがメインイベントループを使用する必要があるという明らかな欠点があり、コールバックが多少複雑になります。また、コールバックの構造化方法に一見恣意的な制限を課します。
  • 逆に言うと、コールバックを受け取るすべての関数は、イベントループを介して実際の "作業"をスケジュールする必要があります
    • これには上記と同じ欠点がありますが、クライアント関係の反対側にあります。
  • triggerコールバックを行う関数は、イベントループを介してのみ呼び出す必要があります。
    • これはもっと単純なルールのように見えますが、実際に従うのがどれほど簡単かはわかりません。
  • コールバック自体は、イベントループを介してのみ呼び出すことができます。
    • これは、上記の原則よりわずかに強力であり、おそらく実施が容易です。これは、「純粋にイベント駆動型」のアーキテクチャのようなものです。

これらすべてにはかなり明らかな欠点があり、アプリケーションのメインループを介してコールバックを延期する何らかの方法なしに機能する戦略は考えられないため、実際にはコールバックを長い間安全に使用する方法がわかりません。 Qtイベントループなどを使用せずにアプリケーションを実行します。


リクエストに従って、ここに私が持っていた特定の問題を説明するためのいくつかのPython風の疑似コードがあります:

class TransactionManager:
    def newTransaction(Callback cb, ...):
        if (can schedule transaction) { scheduleTransaction(cb, ...); }
        else { cb(error); }
    def scheduleTransaction(Callback cb, ...):
        ... // set up the transaction itself and register the callback

class TaskManager:
    def newShortTask(Callback cb, ...):
        // In the original code, an intermediary `ShortTask` object
        // was created, and the `ShortTask` created a separate callback
        // to do some other work in addition to calling `cb`. That's not
        // pertinent to the issue at hand, so it's not included in the
        // pseudocode.
        myTransactionManager.newTransaction(cb, .... );

class LongTask:
    def start():
        myTaskManager.newShortTask(
                lambda (...): self.handleSubtaskFinished(...),
                ....);
    def handleSubtaskFinished(...):
        if (task failed):
            start();  // try again
        else:
            ... // continue with task

問題は、「トランザクション」の成功がハードウェアの状態によって部分的に決定され、ハードウェアが要求されたトランザクションを(一時的に)実行できないときに何が起こるかをテストしていたことです。したがって、操作の順序は次のとおりです。

  • LongTask::start()が呼び出されます。
  • LongTask::startは、TaskManagerを介して新しい「短い」タスクを開始します
  • 新しい「短い」タスクは、TransactionManagerを介して新しいトランザクションをスケジュールします
  • ハードウェアの状態が悪いため、短いタスクはすぐに失敗します。 TransactionManager::newTransaction()が戻る前に、コールバックcbが呼び出されます。
  • LongTask::handleSubtaskFinished()は、そのサブタスクが失敗したことを確認し、すぐに再試行して、このリストの先頭に戻りますスタックを巻き戻すことなく

[〜#〜] note [〜#〜]この場合、「ハードウェアが自身を修正するまでの無限ループ」の動作正しい;問題は、ハードウェア障害が修正されるのを待つ間、アプリケーションがスタック領域を使い尽くさないようにする必要があることです。

6
Kyle Strand

問題は、意図しない無限再帰の場合と同じ問題だと思います。呼び出し時に呼び出し先が何をするかは完全にはわからず、呼び出し先が変更されていない引数で呼び出し元を呼び出すと、運命にあります。私は本当にあなたのケースが違うとは思いません、関係するメカニズムだけが異なります。

だから、私は一般的に再帰に関してあなたの状況に同じことを適用します:そのようなスキームはa)lotの正当なコードを禁止して望ましくない動作を避けるので、それを一般的に禁止するのは良い考えではありません一部の特殊なケースでは、b)タイトジャケットのように厳格でない限り、望ましくない動作を回避するには不十分です。代わりに、関数呼び出しについて考えることで無限再帰を回避し、同様に、コールバック登録について考えることで無限コールバック再帰を回避する必要があります。

私はあなたが持っていた問題を100%確信していませんが、コールバックがコールバックを呼び出すか、少なくともコールバックが順番通りに呼び出されない問題があったようです。

それが発生した理由は、コールバックベースのアーキテクチャをイベントベースのアーキテクチャと組み合わせているため、プログラムのフローを完全に制御できないためです。

簡単な答えは、どちらか一方を使用することです。それらを混合する必要がある場合は、非常に注意して非常に特殊なケースでのみ使用してください。

0
gbjbaanb