Come pianifichi il tuo codice asincrono?

7

Ho creato una libreria che è un invocatore per un servizio web da qualche altra parte. La libreria espone metodi asincroni, dal momento che le chiamate al servizio web sono un buon candidato per questo.

All'inizio tutto andava bene, avevo metodi con operazioni facili da capire in un modo CRUD, dato che la libreria è una specie di repository.

Ma la logica aziendale ha iniziato a diventare complessa e alcune procedure implicano il concatenamento di molte di queste operazioni asincrone, a volte con percorsi diversi a seconda del valore del risultato, ecc. ecc.

All'improvviso, tutto è molto disordinato, per fermare l'esecuzione in un punto di rottura non è molto utile, per scoprire cosa sta succedendo o dove nella tempistica del processo hai smesso di diventare un dolore ... Lo sviluppo diventa meno veloce , meno agile, e catturare quegli insetti che si verificano una volta ogni 1000 volte diventa un inferno.

Dal punto tecnico, un repository che espone metodi asincroni sembrava una buona idea, perché alcuni livelli di persistenza potevano avere ritardi, e puoi usare l'approccio asincrono per sfruttare al massimo il tuo hardware. Ma dal punto di vista funzionale, le cose sono diventate molto complesse e considerando quelle procedure in cui erano necessarie una dozzina di chiamate diverse ... Non conosco il reale valore del miglioramento.

Dopo aver letto su TPL per un po ', mi è sembrata una buona idea per la gestione delle attività, ma nel momento in cui devi combinarle e iniziare a riutilizzare le funzionalità esistenti, le cose diventano molto complicate. Ho avuto una buona esperienza nell'usarlo per scenari molto concreti, ma ho avuto esperienze negative nell'utilizzarli a grandi linee.

Come lavori in modo asincrono? Lo usi sempre? O solo per processi di lunga durata?

    
posta NullOrEmpty 25.06.2013 - 17:51
fonte

4 risposte

7

Progetterei una macchina a stati per ogni insieme di processi collegati e collegati e tengo traccia dello stato da qualche parte in un oggetto di alto livello, quindi è più facile eseguire il debug. È possibile tracciare le transizioni di stato e rilevare transizioni di stato non valide.

    
risposta data 27.06.2013 - 03:52
fonte
3

Come dice Frank, scrivi una macchina a stati per ogni blocco di business logic che richiede più chiamate asincrone. Quindi, è facile interrogare lo stato durante il debug, perché lo hai enumerato.

In effetti, inizia semplicemente disegnando il diagramma di stato. Se il diagramma è troppo complesso (specialmente se hai diverse chiamate asincrone durante il volo che potrebbero tornare in vari ordini), scrivi come tabella per assicurarti di aver gestito ogni combinazione possibile.

    
risposta data 27.06.2013 - 11:25
fonte
1

Primo: pianifichi e sviluppi un codice sincrono che funzioni perfettamente da solo. Quindi si isolano le parti che devono essere reinserite e si creano punti di accesso per quelle parti di codice.

Questo approccio è il migliore, perché aggiungerai il meccanismo di sincronizzazione solo su richiesta.

    
risposta data 27.06.2013 - 14:46
fonte
1

Se hai la possibilità di usare le coroutine (con rese a profondità arbitrarie dello stack di chiamate) o continuazioni, queste ti permettono di scrivere codice asincrono che assomiglia molto al codice sincrono; tutto ciò che una chiamata asincrona deve fare per sembrare sincrono è sospendere l'esecuzione dopo aver effettuato la chiamata e pianificare la ripresa dell'esecuzione per quando la risposta alla chiamata asincrona diventa disponibile. Ciò riduce notevolmente la complessità apparente complessiva. (La complessità in realtà non scompare, ma il linguaggio / runtime gestisce la maggior parte delle parti più complesse, quindi non è necessario.)

È possibile simulare questo genere di cose con i thread. Questo mi lascia sempre un po 'sporco, dato che ritengo che avere molti thread con I / O siano un'indicazione che il design del sistema non è corretto.

In caso contrario, l'approccio migliore è utilizzare lo stile di passaggio continuo in cui dividi il codice in parti sincrone e passa qualsiasi stato condiviso necessario tra di loro (nei periodi in cui stai aspettando per l'elaborazione asincrona per produrre un risultato) utilizzando un oggetto di contesto di qualche tipo. Questo è un po 'imbarazzante da comprendere in astratto: ecco un esempio un po' più concreto. Considera questo codice sincrono (pseudo-):

function DoWork() {
    let A = "foo";
    let B = 1.23;
    Call1(the.arguments);
    A.append(B);
    Call2(the.arguments);
    B += A.length;
}

DoWork();
print "finished";

Se cambiamo questo stile in passaggio continuo, otteniamo qualcosa del genere:

function DoWork(doneCallback) {
    let state = new Context();
    state.A = "foo";
    state.B = 1.23;
    state.done = doneCallback;
    DoAsyncCall1(the.arguments, callback=fn()=>DoMoreWork(state));
}
function DoMoreWork(state) {
    state.A.append(state.B);
    DoAsyncCall2(the.arguments, callback=fn()=>DoneTheWork(state));
}
function DoneTheWork(state) {
    state.B += state.A.length;
    state.done(state.A, state.B);
}

DoWork(fn(…)=>print "finished");

Ogni sezione tra le chiamate asincrone diventa la sua funzione (dovresti anche fare l'inizio di ogni ciclo che hai - purché abbia una chiamata asincrona - essere l'inizio di una funzione in modo da può modellare il ciclo richiamando). Il state che viene passato in giro è la chiave per far funzionare tutto questo ed è possibile (presupponendo un meccanismo di chiamata asincrono non orribile) di avere più insiemi di elaborazione in corso in una volta, poiché ogni parte ne conosce solo la sua piccolo mondo. Tuttavia, è importante cercare di evitare l'uso dello stato globale quando si esegue questa operazione, se per nessun altro motivo ti lascerà molto confuso. È molto più semplice far funzionare tutto questo se si utilizzano tecniche di programmazione in gran parte funzionali.

    
risposta data 27.06.2013 - 17:23
fonte

Leggi altre domande sui tag