Buoni modi per passare a uno stato particolare in una funzione stateful cedevole?

5

Sto lavorando su alcuni codici incorporati usando C. Varie funzionalità richiedono funzioni di stato non bloccanti, che sono per lo più implementate usando un interruttore su vari stati. Ad esempio, un gestore di connessione modem (pseudo-codice):

void manage_connection(void) {
    static states state = IDLE;
    switch (state) {
        case IDLE:
            if (connection_requested()) state = BACKOFF;
            break;
        case BACKOFF:
            if (random_delay_timeout()) {
                do_connect();
                state = CONNECTED;
            }
            break;
        case CONNECTED:
            result = send_data();
            if (result == DATA_SENT) {
                state = IDLE;
            } else if (result == ERROR) {
                // Go straight to backoff state and connect ASAP
                state = BACKOFF;
            }
            break;
    }
}

Le macchine di stato sono cattive nei momenti migliori, quindi ho iniziato a passare a un paradigma diverso usando le funzioni in stile coroutine, usando una dichiarazione di "rendimento". C, C non fornisce una dichiarazione di "rendimento", ma sto usando protothreads per raggiungere questo obiettivo. Protothreads è fantastico e mi dà esattamente quello che voglio implementare la macchina a stati non bloccanti di cui sopra in uno stile di programmazione strutturata. La funzione sopra riportata si traduce approssimativamente in (pseudo-codice):

void manage_connection(void) {
    WAIT_UNTIL(connection_requested()); // or, while(!condition) yield;
    WAIT_UNTIL(random_delay_timeout());
    do_connect();
    WAIT_UNTIL((result = send_data()));

    if (result == DATA_SENT) {
        // RESTART starts from top. It is part of the protothreads API.
        RESTART();
    } else if (result == ERROR) {
        // This is the part which doesn't translate so well.
        // Could use an ugly goto:
        goto backoff;
        // Or similar alternatives, none of which scale well:
        // - Set a flag to skip the states I don't want, and RESTART();
        // - Use while loops or other control logic to branch appropriately.
    }
}

Come puoi vedere, la versione dei protothread è, nel complesso, molto più chiara e amp; più semplice in termini di flusso di logica (una volta che si impara come funzionano i protothread e si guarda oltre un numero di piastre molto minore che non ho incluso nell'esempio).

Ma saltare a un particolare stato in una macchina a stati è un modello comune che mi viene in mente, che la versione della macchina a stati gestisce in modo elegante, ma la versione procedurale no. Naturalmente, è per questo che la versione procedurale è meno incline ai bug e più chiara - perché non puoi saltare per tutto il luogo per un capriccio (eccetto per goto, che vorrei evitare per le solite ragioni).

Tuttavia, quando hai una buona ragione per voler saltare, aggiunge una significativa mancanza di chiarezza soggetta a bug in quella che altrimenti è una funzione molto semplice.

Ovviamente ci sono molti modi per ottenere le funzionalità necessarie, ma nessuno di essi è in grado di adattarsi a più di pochi stati. Tieni presente che questo è l'esempio più semplice possibile: le mie funzioni del mondo reale riempiono metà schermo.

Quali sono alcuni modi per risolvere questo problema in modo da non confondere il flusso della logica altrimenti lucido?

    
posta bryhoyt 04.05.2012 - 02:47
fonte

2 risposte

3

Sfortunatamente, non penso che ci sia un modo chiaro per uscire da questo in chiaro C.

Il tuo "flusso di logica altrimenti lucido" (sia implementato con protothread, thread o fibre) è ancora una macchina a stati (come qualsiasi programma), che è solo apparentemente pulito, perché è mancano i suoi casi laterali: transizioni in stati di errore, o transizioni indietro (in caso di guasto o timeout); se si collegano queste transizioni, si scopre che è necessario goto o confondere il progetto, che, come è stato osservato, tende a diventare brutto.

Una soluzione potenzialmente più pulita è quella di configurare una macchina dello stato dichiarativo : è possibile utilizzare una serie di voci di transizione di stato, ciascuna voce che contiene lo stato di origine, lo stato finale e almeno due funzioni puntatori: un correttore di transizione (posso eseguire questa transizione?) e un'azione di transizione (cosa fare quando viene eseguita la transizione). Quando è necessario eseguire una transizione, passare attraverso le voci dell'array con state of origin == current state , eseguire il primo per il quale il correttore restituisce TRUE , eseguirne l'azione e passare al nuovo stato. Ricorda che le pedine dovrebbero essere leggere o NULL (forse solo il controllo del contesto - perché le esegui sempre), e le azioni dovrebbero essere pesanti (fare cose).

Questo è un buon modello per gli SM densi (con molte transizioni); il suo unico svantaggio è che separa l'implementazione (i controllori e le azioni) dalla dichiarazione SM (che può portare a un po 'di yo-yo quando si cerca di capire cosa fa).

Puoi migliorare ulteriormente questo modello con cose come:

  • Azioni che eseguono ogni volta viene raggiunto uno stato (indipendentemente dallo stato di origine), prima o dopo la transizione;
  • Eccezioni (azioni che vengono eseguite quando la transizione segnala un errore in qualche modo);
  • Nested SMs
risposta data 04.05.2012 - 10:13
fonte
2

Prima di tutto, grazie per avermi permesso di scoprire protothread : molto intelligente! Questo significa anche che sono tutto tranne un esperto di protothread .

Penso che scriverei la tua macchina a stati come segue:

void manage_connection(void) {
    for (;;) {
        WAIT_UNTIL(connection_requested());
        do {
            if (attempts++ >= MAX)
                RESTART();
            WAIT_UNTIL(random_delay_timeout());
            do_connect();
            WAIT_UNTIL(result = send_data());
        } while (result != DATA_SENT);
    }
}

Il punto è che è necessario un ramo condizionale per gestire un caso non lineare. Inoltre, i tentativi di connessione non possono essere infiniti.

    
risposta data 07.05.2012 - 22:34
fonte

Leggi altre domande sui tag