Perché abbiamo bisogno della parola chiave asincrona?


19

Ho appena iniziato a giocare con asincrono / wait in .Net 4.5. Una cosa di cui sono inizialmente curioso, perché è necessaria la parola chiave asincrona? La spiegazione che ho letto è che si tratta di un marker, quindi il compilatore sa che un metodo attende qualcosa. Ma sembra che il compilatore dovrebbe essere in grado di capirlo senza una parola chiave. Quindi cos'altro fa?


2
Ecco un ottimo articolo (serie) di blog che fornisce alcuni dettagli che descrivono la parola chiave in C # e le funzionalità correlate in F #.
paul

Penso che sia principalmente un marcatore per lo sviluppatore e non per il compilatore.
CodesInChaos,


@svick grazie, questo è quello che stavo cercando. Ha perfettamente senso ora.
ConditionRacer

Risposte:


23

Ci sono diverse risposte qui e tutte parlano di ciò che fanno i metodi asincroni, ma nessuna di esse risponde alla domanda, motivo per cui asyncè necessaria come parola chiave inclusa nella dichiarazione di funzione.

Non è "dirigere il compilatore per trasformare la funzione in un modo speciale"; awaitda solo potrebbe farlo. Perché? Poiché C # ha già un altro meccanismo in cui la presenza di una parola chiave speciale nel corpo metodo causa il compilatore di eseguire estrema (e molto simile a async/await) trasformazioni sul corpo del metodo: yield.

Tranne che yieldnon è la sua parola chiave in C #, e capire perché spiegherà asyncanche. A differenza della maggior parte delle lingue che supportano questo meccanismo, in C # non puoi dire che yield value;devi dire yield return value;invece. Perché? Perché è stato aggiunto al linguaggio dopo che C # esisteva già, ed era abbastanza ragionevole presumere che qualcuno, da qualche parte, avrebbe potuto usare yieldcome nome di una variabile. Ma poiché non esisteva uno scenario preesistente in cui <variable name> returnfosse sintatticamente corretto, è yield returnstato aggiunto al linguaggio per consentire l'introduzione di generatori mantenendo al 100% la compatibilità con il codice esistente.

Ed è per questo che è asyncstato aggiunto come un modificatore di funzione: per evitare di violare il codice esistente utilizzato awaitcome nome di variabile. Poiché non asyncesistevano già metodi, nessun vecchio codice è invalidato e nel nuovo codice, il compilatore può utilizzare la presenza del asynctag per sapere che awaitdeve essere trattato come una parola chiave e non come un identificatore.


3
Questo è praticamente ciò che dice anche questo articolo (originariamente pubblicato da @svick nei commenti), ma nessuno lo ha mai pubblicato come risposta. Ci sono voluti solo due anni e mezzo! Grazie :)
ConditionRacer

Ciò non significa che in alcune versioni future, il requisito di specificare la asyncparola chiave può essere eliminato? Ovviamente si tratterebbe di un'interruzione di BC, ma si potrebbe pensare che riguardi pochissimi progetti e che sia un requisito semplice per l'aggiornamento (ovvero la modifica di un nome di variabile).
Quolonel Domande

6

cambia il metodo da un metodo normale a un oggetto con callback che richiede un approccio totalmente diverso per la generazione del codice

e quando succede qualcosa di così drastico, è consuetudine significarlo chiaramente (abbiamo imparato quella lezione dal C ++)


Quindi questo è davvero qualcosa per il lettore / programmatore, e non tanto per il compilatore?
ConditionRacer

@ Justin984 aiuta sicuramente a segnalare al compilatore che deve accadere qualcosa di speciale
maniaco del cricchetto,

Immagino di essere curioso del perché il compilatore ne abbia bisogno. Non potrebbe semplicemente analizzare il metodo e vedere che l'attesa è da qualche parte nel corpo del metodo?
ConditionRacer

aiuta quando si desidera programmare più messaggi asynccontemporaneamente in modo che non vengano eseguiti uno dopo l'altro ma contemporaneamente (se almeno sono disponibili thread)
maniaco del cricchetto

@ Justin984: non tutti i metodi di restituzione hanno Task<T>effettivamente le caratteristiche di un asyncmetodo: ad esempio, potresti semplicemente eseguire alcune logiche aziendali / di fabbrica e quindi delegare ad altri metodi di restituzione Task<T>. asynci metodi hanno un tipo di ritorno Task<T>nella firma ma in realtà non restituiscono a Task<T>, restituiscono semplicemente a T. Per capire tutto questo senza la asyncparola chiave, il compilatore C # dovrebbe fare ogni sorta di ispezione approfondita del metodo, che probabilmente lo rallenterebbe un po 'e porterebbe a ogni sorta di ambiguità.
Aaronaught,

4

L'idea generale con parole chiave come "asincrono" o "non sicuro" è quella di rimuovere l'ambiguità sul modo in cui il codice che modificano dovrebbe essere trattato. Nel caso della parola chiave asincrona, indica al compilatore di trattare il metodo modificato come qualcosa che non deve essere restituito immediatamente. Ciò consente al thread in cui viene utilizzato questo metodo di continuare senza dover attendere i risultati di quel metodo. È effettivamente un'ottimizzazione del codice.


Sono tentato di sottovalutare perché, mentre il primo paragrafo è corretto, il confronto con gli interrupt è errato (secondo il mio parere informato).
paul

Li vedo in qualche modo analoghi in termini di scopo, ma lo rimuoverò per motivi di chiarezza.
Ingegnere mondiale il

Gli interrupt sono più simili agli eventi che al codice asincrono nella mia mente: causano un cambio di contesto ed eseguono fino al completamento prima che il codice normale venga ripreso (e anche il codice normale potrebbe ignorare completamente la sua esecuzione), mentre con il codice asincrono il metodo potrebbe non completato prima che il thread chiamante venga ripreso. Vedo quello che stavi cercando di dire con diversi meccanismi che richiedono diverse implementazioni sotto il cofano, ma gli interrupt non mi hanno fatto impazzire per illustrare questo punto. (+1 per il resto, tra.)
paul

0

OK, ecco la mia opinione su di esso.

C'è qualcosa chiamato coroutine che è noto da decenni. ("Classe di Knuth e Hopper" "per decenni") Sono generalizzazioni di subroutine , in quanto non solo ottengono e rilasciano il controllo all'inizio della dichiarazione e del ritorno della funzione, ma lo fanno anche in punti specifici ( punti di sospensione ). Una subroutine è una coroutine senza punti di sospensione.

Sono FACILMENTE FACILI da implementare con le macro C, come mostrato nel seguente documento sui "protothreads". ( http://dunkels.com/adam/dunkels06protothreads.pdf ) Leggi. Aspetterò...

La linea di fondo è che le macro creano un grande switche caseun'etichetta in ogni punto di sospensione. Ad ogni punto di sospensione, la funzione memorizza il valore casedell'etichetta immediatamente successiva , in modo che sappia dove riprendere l'esecuzione la prossima volta che viene chiamata. E restituisce il controllo al chiamante.

Questo viene fatto senza modificare il flusso apparente di controllo del codice descritto nel "protothread".

Immagina ora di avere un grande loop che chiama a turno tutti questi "protothreads" e che esegui contemporaneamente "protothreads" su un singolo thread.

Questo approccio presenta due inconvenienti:

  1. Non è possibile mantenere lo stato nelle variabili locali tra le riprese.
  2. Non è possibile sospendere il "protothread" da una profondità di chiamata arbitraria. (tutti i punti di sospensione devono essere al livello 0)

Esistono soluzioni alternative per entrambi:

  1. Tutte le variabili locali devono essere richiamate nel contesto del protothread (contesto che è già necessario per il fatto che il protothread deve memorizzare il prossimo punto di ripresa)
  2. Se ritieni di dover davvero chiamare un altro protothread da un protothread, "spawn" un protothread bambino e sospendi fino al completamento del bambino.

E se avessi il supporto del compilatore per eseguire il lavoro di riscrittura che fanno le macro e la soluzione alternativa, puoi semplicemente scrivere il tuo codice protothread proprio come intendi e inserire punti di sospensione con una parola chiave.

E questo è tutto asynce di che awaitsi tratta: creazione di coroutine (senza stack).

Le coroutine in C # sono reificate come oggetti della classe (generica o non generica) Task.

Trovo queste parole chiave molto fuorvianti. La mia lettura mentale è:

  • async come "sospettabile"
  • await come "sospendi fino al completamento di"
  • Task come "futuro ..."

Adesso. Dobbiamo davvero contrassegnare la funzione async? Oltre a dire che dovrebbe innescare i meccanismi di riscrittura del codice per rendere la funzione un coroutine, risolve alcune ambiguità. Considera questo codice.

public Task<object> AmIACoroutine() {
    var tcs = new TaskCompletionSource<object>();
    return tcs.Task;
}

Supponendo che asyncnon sia obbligatorio, si tratta di una funzione di routine o normale? Il compilatore dovrebbe riscriverlo come coroutine o no? Entrambi potrebbero essere possibili con una diversa semantica finale.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.