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:
- Le condizioni di gara sono possibili quando due coroutine in parallelo condividono lo stato comune
- Le informazioni temporali sullo stato transitorio / non risolto potrebbero essere incorporate in oggetti asincroni, che possono imporre determinate regole di ordinamento
- 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?