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" tramiteLongTask::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 callbackTransactionManager::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.