C++/Qtアプリケーションでスタックオーバーフローをデバッグするために、長くて悲惨な1週間を過ごしました。基本的な問題は、コールバックを受け入れる関数があり、特定のケースでは、元の関数が戻る前に(別の関数で)コールバックがトリガーされたことです。この場合、コールバックにより、コールバック自体が完了する前に、同じコールバックが登録されていました。
私がQtイベントループを使用しているため、解決策は、コールバックを直接呼び出すのではなく、シングルショットQTimerによってトリガーされるようにスケジュールすることでした。 (Qtを使用したことがない人にとって、タイムアウトが0ミリ秒のシングルショットQTimerは、作業ユニットがイベントループによってトリガーされることを保証する方法にすぎません。Boost-Asioのio_service::post
とほぼ同じです。)または、コールバック自体が直接呼び出すのではなく、コールバック登録関数をスケジュールすることもできます。
これは既知の問題ですか?それを処理する標準的な方法はありますか?
役立つと思われるいくつかの可能なベストプラクティスガイドラインがあります。
これらすべてにはかなり明らかな欠点があり、アプリケーションのメインループを介してコールバックを延期する何らかの方法なしに機能する戦略は考えられないため、実際にはコールバックを長い間安全に使用する方法がわかりません。 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 [〜#〜]この場合、「ハードウェアが自身を修正するまでの無限ループ」の動作は正しい;問題は、ハードウェア障害が修正されるのを待つ間、アプリケーションがスタック領域を使い尽くさないようにする必要があることです。
問題は、意図しない無限再帰の場合と同じ問題だと思います。呼び出し時に呼び出し先が何をするかは完全にはわからず、呼び出し先が変更されていない引数で呼び出し元を呼び出すと、運命にあります。私は本当にあなたのケースが違うとは思いません、関係するメカニズムだけが異なります。
だから、私は一般的に再帰に関してあなたの状況に同じことを適用します:そのようなスキームはa)lotの正当なコードを禁止して望ましくない動作を避けるので、それを一般的に禁止するのは良い考えではありません一部の特殊なケースでは、b)タイトジャケットのように厳格でない限り、望ましくない動作を回避するには不十分です。代わりに、関数呼び出しについて考えることで無限再帰を回避し、同様に、コールバック登録について考えることで無限コールバック再帰を回避する必要があります。
私はあなたが持っていた問題を100%確信していませんが、コールバックがコールバックを呼び出すか、少なくともコールバックが順番通りに呼び出されない問題があったようです。
それが発生した理由は、コールバックベースのアーキテクチャをイベントベースのアーキテクチャと組み合わせているため、プログラムのフローを完全に制御できないためです。
簡単な答えは、どちらか一方を使用することです。それらを混合する必要がある場合は、非常に注意して非常に特殊なケースでのみ使用してください。