Perché molte lingue distinguono semanticamente le funzioni "asincrone" da quelle "non asincrone"?

6

Ho visto questo in C #, Hack e ora Kotlin: await o un'operazione equivalente può essere eseguita solo in speciali contesti "asincroni". I valori di ritorno da questi sono, per prendere a prestito la terminologia di Hack, "a rischio" a turno, quindi l'asincronizzazione speciale di alcune funzioni di sistema asincrone di basso livello richiama le bolle verso l'alto a meno che non si trasformi in un'operazione sincrona. Questa partizione del codebase in ecosistemi sincroni e asincroni. Dopo aver lavorato per un po 'con async-await in Hack, tuttavia, sto iniziando a mettere in discussione la necessità. Perché l'ambito di chiamata deve sapere che sta chiamando una funzione asincrona? Perché le funzioni asincrone non possono assomigliare alle funzioni di sincronizzazione che capita di lanciare il controllo da qualche altra parte occasionalmente?

Ho trovato tutta la particolarità del codice asincrono che ho scritto derivare da tre conseguenze:

  1. Le condizioni di gara sono possibili quando due coroutine in parallelo condividono lo stato comune
  2. Le informazioni temporali sullo stato transitorio / non risolto potrebbero essere incorporate in oggetti asincroni, che possono imporre determinate regole di ordinamento
  3. Il lavoro sottostante di una coroutine può essere mutato da altre coroutine (ad esempio cancellazione)

Concederò che il primo è allettante. Annotazione di un ecosistema come async urla "attenzione: le condizioni di gara potrebbero vivere qui!" Tuttavia, l'attenzione alle condizioni di gara può essere completamente localizzata per combinare le funzioni (ad esempio (Awaitable<Tu>, Awaitable<Tv>, ...) -> Awaitable<(Tu, Tv, ...)> ) poiché senza di esse, due coroutine non possono essere eseguite in parallelo. Quindi, il problema diventa molto specifico: "assicurati che tutti i termini di questa funzione combinatoria non vadano di corsa". Questo è utile per la chiarezza. Fintanto che è chiaro che la combinazione di funzioni è utile per il codice asincrono (ma ovviamente non limitato ad esso, il codice asincrono è un superset del codice di sincronizzazione), e c'è un numero finito di elementi canonici (i costrutti del linguaggio come spesso sono), I senti che meglio comunica i rischi delle condizioni di gara localizzando le loro fonti.

Gli altri due sono una questione di design del linguaggio di come vengono rappresentati gli oggetti asincroni di livello più basso (per esempio Hack's WaitHandle s). Qualsiasi mutazione di un oggetto asincrono di alto livello è necessariamente confinata a un insieme di operazioni rispetto agli oggetti asincroni di basso livello sottostanti provenienti dalle chiamate di sistema. Indipendentemente dal fatto che l'ambito di chiamata sia sincrono è irrilevante, poiché la mutabilità e gli effetti della mutazione sono puramente funzioni di quello stato sottostante in un singolo punto nel tempo. L'aggregazione di questi in un asincrono oggetto anonimo non rende il comportamento più chiaro - semmai, a mio avviso, lo oscura con l'illusione del determinismo. Tutto ciò è discutibile quando lo scheduler è opaco (come in Hack e, da quello che io raccolgo, anche Kotlin) dove le informazioni e i mutatori sono comunque nascosti.

Altrimenti, il risultato è lo stesso per l'ambito di chiamata: alla fine ottiene un valore o un'eccezione e fa la sua cosa sincrona. Mi manca una parte del pensiero progettuale dietro questa regola? In alternativa, ci sono esempi in cui i contratti di funzione asincrona sono indistinguibili da quelli sincroni?

    
posta concat 22.03.2017 - 20:43
fonte

3 risposte

5

Il motivo per cui devi contrassegnare i metodi come async in C # per utilizzare await come parola chiave all'interno di essi è che C # era già una lingua consolidata nel momento in cui è stata aggiunta come nuova funzione, ed è ragionevole supporre che ci fosse del codice là fuori che usasse await come identificatore, che si sarebbe rotto sotto il nuovo sistema.

Introducendo una nuova sintassi che non era mai stata valida prima (metodi contrassegnati come async nella dichiarazione del metodo), il team del compilatore C # poteva garantire che tutto il codice esistente continuasse a funzionare come al solito e l'uso di await come pseudo-parola chiave entrerebbe in gioco solo quando il coder lo ha esplicitamente richiesto nel codice scritto per la nuova funzione.

Altre lingue probabilmente lo hanno fatto in quel modo per motivi simili, o "perché è così che C # ha fatto questo."

    
risposta data 22.03.2017 - 21:24
fonte
5

Penso che tu stia leggendo troppo in questo. async cambia il tipo di ritorno di una funzione. Non so come o se C # denota che oltre il async , ma in Scala (e credo che Kotlin, ma non ho familiarità con esso) il tuo tipo di ritorno va letteralmente da A a Future[A] . Ovviamente, ciò significa che il contesto chiamante deve generare un codice diverso per recuperare il valore restituito.

Non sarebbe difficile in Scala aggiungere conversioni implicite per passare da A a Future[A] dove è previsto un Future[A] nel contesto chiamante, semplicemente avvolgendolo in un blocco async {} . Allo stesso modo, sarebbe banale aggiungere conversioni implicite per andare nella direzione opposta avvolgendo una chiamata Await.result . Altre lingue potrebbero facilmente fare questa conversione nel compilatore.

Questo sarebbe un errore, però, perché stai buttando via tutto l'aiuto che il tuo correttore di tipi ti fornisce per scrivere codice asincrono e controllando precisamente il punto in cui vuoi che diventi sincrono. Il compilatore ti obbliga a mantenere tutto il codice asincrono in async blocchi, quindi quando accidentalmente vai più sincrono di quanto intendi nello stack di chiamate può darti un utile messaggio di errore. Con i tipi più deboli vengono le protezioni più deboli.

In altre parole, è fondamentalmente la stessa ragione per cui non vuoi che il tuo compilatore converta automaticamente le stringhe in numeri interi. Quando si tratta di qualcosa come un errore come codice asincrono, si vuole che il compilatore ti dia il maggior aiuto possibile.

    
risposta data 23.03.2017 - 03:49
fonte
0

Async e attendere sono solo zucchero sintattico. Non devi usarli, ma il codice è più pulito e più leggibile se lo fai.

Quando contrassegni una funzione come asincrona, il compilatore inietta un gruppo di codice aggiuntivo. Potresti aver scritto tu stesso quel codice, ma segue sempre lo stesso schema e può essere fonte di distrazione durante la lettura del codice.

    
risposta data 24.03.2017 - 08:22
fonte

Leggi altre domande sui tag