Le linee guida dell'uso asincrono / atteso in C # non sono in contraddizione con i concetti di buona architettura e stratificazione dell'astrazione?

101

Questa domanda riguarda il linguaggio C #, ma mi aspetto che copra altri linguaggi come Java o TypeScript.

Microsoft consiglia le best practice sull'utilizzo di chiamate asincrone in .NET. Tra questi suggerimenti, scegliamo due:

  • cambia la firma dei metodi asincroni in modo che restituiscano Attività o Attività < > (in TypeScript, sarebbe una Promessa < >)
  • cambia i nomi dei metodi asincroni per terminare con xxxAsync ()

Ora, quando si sostituisce un componente sincrono di basso livello con uno asincrono, questo influenza lo stack completo dell'applicazione. Poiché async / await ha un impatto positivo solo se usato "fino in fondo", significa che i nomi delle firme e dei metodi di ogni livello nell'applicazione devono essere modificati.

Una buona architettura spesso implica l'inserimento di astrazioni tra ogni livello, in modo tale che la sostituzione di componenti di basso livello da parte di altri non venga vista dai componenti di livello superiore. In C #, le astrazioni prendono la forma di interfacce. Se introduciamo un nuovo componente asincrono di basso livello, ogni interfaccia nello stack di chiamate deve essere modificata o sostituita da una nuova interfaccia. Il modo in cui un problema viene risolto (asincrono o sincronizzazione) in una classe di implementazione non è più nascosto (astratto) ai chiamanti. I chiamanti devono sapere se è sincronizzato o asincrono.

Non sei asincrono / attendi le migliori pratiche in contraddizione con i principi della "buona architettura"?

Significa che ogni interfaccia (ad esempio IEnumerable, IDataAccessLayer) ha bisogno della loro controparte asincrona (IAsyncEnumerable, IAsyncDataAccessLayer) in modo tale che possano essere sostituiti nello stack quando si passa alle dipendenze asincrone?

Se spingiamo il problema un po 'più avanti, non sarebbe più semplice ipotizzare che ogni metodo sia asincrono (per restituire un Task < > o Promise < >), e per i metodi per sincronizzare le chiamate asincrone quando non sono effettivamente asincroni? È qualcosa che ci si aspetta dai futuri linguaggi di programmazione?

    
posta corentinaltepe 05.12.2018 - 09:22
fonte

5 risposte

109

Di che colore è la tua funzione?

Potresti essere interessato a Che colore è la tua funzione 1 .

In questo articolo, descrive un linguaggio fittizio in cui:

  • Ogni funzione ha un colore: blu o rosso.
  • Una funzione rossa può chiamare funzioni blu o rosse, nessun problema.
  • Una funzione blu può solo chiamare funzioni blu.

Anche se fittizio, questo accade abbastanza regolarmente nei linguaggi di programmazione:

  • In C ++, un metodo "const" può solo chiamare altri metodi "const" su this .
  • In Haskell, una funzione non IO può chiamare solo funzioni non IO.
  • In C #, una funzione di sincronizzazione può solo chiamare le funzioni di sincronizzazione 2 .

Come hai capito, a causa di queste regole, le funzioni rosse tendono a diffondere attorno al codice base. Ne inserisci uno e poco a poco colonizza l'intero codice base.

1 Bob Nystrom, oltre al blogging, fa anche parte del team di Dart e ha scritto questa piccola serie di Crafting Interpreters; altamente raccomandato per qualsiasi linguaggio di programmazione / compiler afficionado.

2 Non proprio vero, come si può chiamare una funzione asincrona e bloccare finché non ritorna, ma ...

Limitazione della lingua

Questa è, in sostanza, una limitazione della lingua / tempo di esecuzione.

La lingua con il threading M: N, ad esempio, come Erlang e Go, non ha le funzioni async : ogni funzione è potenzialmente asincrona e la sua "fibra" sarà semplicemente sospesa, scambiata e reinserita quando è di nuovo pronto.

C # è andato con un modello di threading 1: 1, e quindi ha deciso di affinare la sincronicità nella lingua per evitare di bloccare accidentalmente i thread.

In presenza di limiti linguistici, le linee guida di codifica devono adattarsi.

    
risposta data 05.12.2018 - 13:58
fonte
81

Hai ragione, qui c'è una contraddizione, ma non è che le "migliori pratiche" siano cattive. È perché la funzione asincrona fa una cosa essenzialmente diversa rispetto a una sincrona. Invece di attendere il risultato dalle sue dipendenze (di solito qualche IO) crea un compito che deve essere gestito dal ciclo degli eventi principale. Questa non è una differenza che può essere ben nascosta sotto l'astrazione.

    
risposta data 05.12.2018 - 10:58
fonte
6

Un metodo asincrono si comporta diversamente da uno che è sincrono, come sono sicuro che tu sappia. In runtime, convertire una chiamata asincrona in una sincrona è banale, ma non si può dire il contrario. Quindi, quindi, la logica diventa, perché non creiamo metodi asincroni di ogni metodo che potrebbe averlo richiesto e che il chiamante "converta" come necessario per un metodo sincrono?

In un certo senso è come avere un metodo che genera eccezioni e un altro che è "sicuro" e non getterebbe nemmeno in caso di errore. A che punto il programmatore è eccessivo nel fornire questi metodi che altrimenti possono essere convertiti l'uno nell'altro?

In questo ci sono due scuole di pensiero: una è quella di creare più metodi, ognuno dei quali chiama un altro metodo possibilmente privato, consentendo la possibilità di fornire parametri opzionali o modifiche minori al comportamento come essere asincroni. L'altro consiste nel ridurre al minimo i metodi di interfaccia per scoprire gli elementi essenziali lasciandoli al chiamante per eseguire da sé le modifiche necessarie.

Se sei della prima scuola, c'è una certa logica nel dedicare una classe alle chiamate sincrone e asincrone per evitare di raddoppiare ogni chiamata. Microsoft tende a favorire questa scuola di pensiero e, per convenzione, per rimanere coerente con lo stile favorito da Microsoft, anche tu dovresti avere una versione Async, più o meno allo stesso modo in cui le interfacce iniziano quasi sempre con un "io". Vorrei sottolineare che non è sbagliato , di per sé, perché è meglio mantenere uno stile coerente in un progetto piuttosto che farlo "nel modo giusto" e cambiare radicalmente lo stile per lo sviluppo che aggiungere a un progetto.

Detto questo, tendo a favorire la seconda scuola, che è quella di minimizzare i metodi di interfaccia. Se penso che un metodo possa essere chiamato in modo asincrono, il metodo per me è asincrono. Il chiamante può decidere se attendere o meno che l'attività finisca prima di procedere. Se questa interfaccia è un'interfaccia per una libreria, è più ragionevole farlo in questo modo per ridurre al minimo il numero di metodi che è necessario deprecare o modificare. Se l'interfaccia è per uso interno nel mio progetto, aggiungerò un metodo per ogni chiamata necessaria nel mio progetto per i parametri forniti e nessun metodo "extra", e anche allora, solo se il comportamento del metodo non è già coperto con un metodo esistente.

Tuttavia, come molte cose in questo campo, è in gran parte soggettivo. Entrambi gli approcci hanno i loro pro e contro. Microsoft ha anche iniziato la convenzione di aggiungere lettere indicative di tipo all'inizio del nome della variabile e "m_" per indicare che si tratta di un membro, che porta a nomi di variabili come m_pUser . Il punto è che nemmeno Microsoft è infallibile e può anche commettere errori.

Detto questo, se il tuo progetto sta seguendo questa convenzione Async, ti consiglierei di rispettarlo e continuare lo stile. E solo una volta che ti viene dato un tuo progetto, puoi scriverlo nel modo migliore che ritieni opportuno.

    
risposta data 05.12.2018 - 10:16
fonte
2

Immaginiamo che ci sia un modo per abilitare a chiamare le funzioni in modo asincrono senza cambiare la loro firma.

Sarebbe davvero bello e nessuno ti consiglierebbe di cambiare i loro nomi.

Ma, le funzioni asincrone effettive, non solo quelle che attendono un'altra funzione asincrona, ma il livello più basso ha una struttura specifica per la loro natura asincrona. ad esempio

public class HTTPClient
{
    public HTTPResponse GET()
    {
        //send data
        while(!timedOut)
        {
            //check for response
            if(response) { 
                this.GotResponse(response); 
            }
            this.YouCanWait();
        }
    }

    //tell calling code that they should watch for this event
    public EventHander GotResponse
    //indicate to calling code that they can go and do something else for a bit
    public EventHander YouCanWait;
}

Sono queste due informazioni che il codice chiamante ha bisogno per eseguire il codice in modo asincrono che cose come Task e async incapsulano.

C'è più di un modo per fare le funzioni asincrone, async Task è solo un modello incorporato nel compilatore tramite i tipi di ritorno in modo che non sia necessario collegare manualmente gli eventi

    
risposta data 05.12.2018 - 12:16
fonte
0

Tratterò il punto principale in modo meno C # ness e più generico:

Aren't async/await best practices contradicting with "good architecture" principles?

Direi che dipende solo dalla scelta che fai nel design della tua API e da ciò che hai lasciato all'utente.

Se vuoi che una funzione della tua API sia solo asincrona, c'è poco interesse nel seguire la convenzione di denominazione. Ritorna sempre Task < > / Promise < > / Future < > / ... come tipo di ritorno, è auto-documentante. Se vuole una risposta sincronizzata, sarà comunque in grado di farlo aspettando, ma se lo fa sempre, fa un po 'di stereotipia.

Tuttavia, se si esegue solo la sincronizzazione dell'API, ciò significa che se un utente desidera che sia asincrono, dovrà gestirne personalmente la parte asincrona.

Questo può fare molto lavoro extra, tuttavia può anche dare più controllo all'utente su quante chiamate simultanee permetta, inserire timeout, retrys e così via.

In un sistema di grandi dimensioni con un'enorme API, implementare la maggior parte di essi per essere sincronizzati per impostazione predefinita potrebbe essere più semplice e più efficiente della gestione indipendente di ciascuna parte dell'API, specialmente se condividono risorse (file system, CPU, database, .. .).

Infatti, per le parti più complesse, potresti perfettamente avere due implementazioni della stessa parte della tua API, una sincrona che fa le cose utili, una asincrona che si basa su quella sincrona che gestisce roba e solo la gestione della concorrenza, carichi, timeout e riprova.

Forse qualcun altro può condividere la sua esperienza con questo perché non ho esperienza con tali sistemi.

    
risposta data 05.12.2018 - 16:44
fonte

Leggi altre domande sui tag