Assicurarsi che la registrazione e l'attivazione dei callback non causino una ricorsione infinita

7

Ho appena trascorso una settimana lunga e misera, eseguendo il debug di uno stack overflow in un'applicazione C ++ / Qt. Il problema fondamentale era che avevo una funzione che accettava un callback e in alcuni casi la richiamata veniva attivata (in un'altra funzione) prima che la funzione originale ritornasse. In questo caso, il callback ha provocato la registrazione di una callback identica prima che la callback stessa sia stata completata.

La soluzione, dal momento che sto usando il ciclo di eventi Qt, era di pianificare l'attivazione del callback da parte di un QTimer single-shot, piuttosto che chiamarlo direttamente. (Per coloro che non hanno usato Qt, un QTimer a colpo singolo con timeout di 0 millisecondi è solo un modo per assicurarsi che un'unità di lavoro sia innescata dal ciclo di eventi: è approssimativamente equivalente a% co_de di Boost-Asio .) In alternativa, il callback stesso avrebbe potuto programmare la funzione di registrazione della richiamata invece di chiamarla direttamente.

Si tratta di un problema noto e c'è un modo standard per affrontarlo?

Ci sono molte possibili linee guida sulle best practice che posso immaginare che potrebbero aiutare:

  • Tutti i callback devono essere ovviamente di breve durata (ad es. impostare i flag e quindi tornare) o programmare un callback "lungo" da attivare direttamente dal ciclo degli eventi principale del programma.
    • Questo ha l'ovvio inconveniente di richiedere ai client di utilizzare un ciclo di eventi principale e rende i callback un po 'più complicati. Inoltre pone restrizioni apparentemente arbitrarie su come possono essere strutturati i callback.
  • Viceversa: tutte le funzioni che richiedono una richiamata devono programmare il loro "lavoro" reale tramite il ciclo degli eventi.
    • Questo ha lo stesso inconveniente di cui sopra, ma dall'altra parte della relazione con il cliente.
  • Funzioni che i callback trigger devono essere richiamati solo tramite il loop eventi.
    • Sembra una regola più semplice, ma non sono sicuro di quanto sarebbe facile da seguire nella pratica.
  • I callback possono essere richiamati solo tramite il ciclo degli eventi.
    • Questo è leggermente più strong del principio di cui sopra e forse più facile da applicare. Sarebbe qualcosa come un'architettura "puramente guidata dagli eventi".

Tutti questi hanno svantaggi piuttosto ovvi, e non riesco a pensare a strategie che funzionino senza un qualche modo di posticipare i callback tramite il ciclo principale dell'applicazione, quindi in effetti non vedo un modo sicuro di usare le callback in un'applicazione di lunga durata senza utilizzare qualcosa come il loop di eventi Qt.

Come da richiesta, ecco alcuni pseudocodi Python-esque per spiegare il particolare problema che ho avuto:

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

Il problema era che il successo di una "transazione" è in parte determinato dallo stato dell'hardware e stavo testando cosa succede quando l'hardware (temporaneamente) non è in grado di eseguire nessuna delle transazioni richieste. L'ordine delle operazioni era quindi:

  • io_service::post è chiamato.
  • LongTask::start() avvia una nuova attività "breve" tramite LongTask::start
  • La nuova attività "breve" pianifica una nuova transazione tramite TaskManager
  • L'attività breve fallisce immediatamente, perché l'hardware è in cattivo stato. Prima che TransactionManager ritorni , viene richiamato il callback TransactionManager::newTransaction() .
  • cb vede che la sua attività secondaria è fallita, quindi riprova immediatamente, tornando all'inizio di questo elenco senza srotolare lo stack .

NOTA che in questo caso il "ciclo infinito fino a quando l'hardware si risolve da solo" comportamento è corretto; il problema è che l'applicazione deve garantire che non esaurisca lo spazio di stack in attesa della correzione dell'errore hardware.

    
posta Kyle Strand 17.04.2016 - 19:49
fonte

2 risposte

1

Penso che il problema sia lo stesso di qualsiasi ricorsione infinita non intenzionale: non sei perfettamente sicuro di cosa faccia il callee quando lo chiami, e se quel callee capita di chiamare il chiamante con argomenti invariati, sei condannato. Io davvero non penso che il tuo caso sia diverso, solo le meccaniche coinvolte differiscono.

Quindi, applicherei lo stesso alla tua situazione come alla ricorsione in generale: non è una buona idea proibirlo in generale, dal momento che qualsiasi schema di questo tipo: a) proibirà un lotto di codice legit a evitare comportamenti indesiderati in alcuni casi particolari eb) essere insufficienti per evitare comportamenti indesiderati a meno che non sia rigoroso come una giacca rigida. Invece, le ricorsioni infinite dovrebbero essere evitate pensando alle chiamate alle funzioni, e allo stesso modo le ricorsioni di callback infinite dovrebbero essere evitate pensando alle tue registrazioni di callback.

    
risposta data 18.04.2016 - 22:40
fonte
0

Non sono sicuro al 100% del problema che hai ma sembra che tu abbia avuto problemi con i callback che chiamavano i callback, o almeno i callback venivano chiamati fuori sequenza.

Perché ciò stava accadendo sembra che tu stia combinando un'architettura basata sul callback con una basata su un evento, quindi non hai il pieno controllo sul flusso del tuo programma.

Una semplice risposta potrebbe essere quella di usare l'una o l'altra e, se si devono mescolare, farlo solo in casi molto specifici con grande cura.

    
risposta data 18.04.2016 - 10:26
fonte