Perché i linguaggi di programmazione non gestiscono automaticamente il problema sincrono / asincrono?


27

Non ho trovato molte risorse al riguardo: mi chiedevo se fosse possibile / una buona idea poter scrivere codice asincrono in modo sincrono.

Ad esempio, ecco un codice JavaScript che recupera il numero di utenti archiviati in un database (un'operazione asincrona):

getNbOfUsers(function (nbOfUsers) { console.log(nbOfUsers) });

Sarebbe bello poter scrivere qualcosa del genere:

const nbOfUsers = getNbOfUsers();
console.log(getNbOfUsers);

E così il compilatore si occuperebbe automaticamente di attendere la risposta e quindi eseguire console.log. Attenderà sempre il completamento delle operazioni asincrone prima che i risultati debbano essere utilizzati altrove. Useremmo molto meno le promesse di callback, asincrono / attendi o altro, e non dovremmo mai preoccuparci se il risultato di un'operazione è disponibile immediatamente o no.

Gli errori sarebbero comunque gestibili (hai nbOfUsersottenuto un numero intero o un errore?) Usando try / catch o qualcosa di simile agli opzionali come nel linguaggio Swift .

È possibile? Potrebbe essere un'idea terribile / un'utopia ... Non lo so.


58
Non capisco davvero la tua domanda. Se "aspetti sempre l'operazione asincrona", non si tratta di un'operazione asincrona, ma di un'operazione sincrona. Puoi chiarire? Forse dare una specifica del tipo di comportamento che stai cercando? Inoltre, "cosa ne pensi" è fuori tema sull'ingegneria del software . Devi formulare la tua domanda nel contesto di un problema concreto, che ha una risposta unica, non ambigua, canonica, obiettivamente corretta.
Jörg W Mittag

4
@ JörgWMittag Immagino un ipotetico C # che implicitamente awaitsa Task<T>per convertirlo inT
Caleth

6
Ciò che proponi non è fattibile. Non sta al compilatore decidere se si desidera attendere il risultato o forse sparare e dimenticare. Oppure corri in background e attendi più tardi. Perché limitarti così?
strano

5
Sì, è un'idea terribile. Basta usare async/ awaitinvece, il che rende esplicite le parti asincrone dell'esecuzione.
Bergi,

5
Quando dici che due cose accadono contemporaneamente, stai dicendo che va bene che queste cose accadano in qualsiasi ordine. Se il tuo codice non ha modo di chiarire quali riordini non infrangono le aspettative del tuo codice, non può renderli simultanei.
Rob

Risposte:


65

Async / waitit è esattamente quella gestione automatizzata che proponi, sebbene con due parole chiave extra. Perché sono importanti? A parte la compatibilità con le versioni precedenti?

  • Senza punti espliciti in cui una coroutine può essere sospesa e ripresa, avremmo bisogno di un sistema di tipi per rilevare dove deve essere atteso un valore attendibile. Molti linguaggi di programmazione non hanno un sistema di questo tipo.

  • Rendendo esplicito l'attesa di un valore, possiamo anche trasmettere valori attendibili come oggetti di prima classe: promesse. Questo può essere super utile quando si scrive un codice di ordine superiore.

  • Il codice asincrono ha effetti molto profondi per il modello di esecuzione di una lingua, simile all'assenza o alla presenza di eccezioni nella lingua. In particolare, una funzione asincrona può essere attesa solo da funzioni asincrone. Questo influenza tutte le funzioni di chiamata! Ma cosa succede se cambiamo una funzione da non asincrona a asincrona alla fine di questa catena di dipendenze? Si tratterebbe di una modifica incompatibile con le versioni precedenti ... a meno che tutte le funzioni non siano sincronizzate e ogni chiamata di funzione sia attesa per impostazione predefinita.

    E questo è altamente indesiderabile perché ha pessime conseguenze sulle prestazioni. Non potresti semplicemente restituire valori economici. Ogni chiamata di funzione diventerebbe molto più costosa.

Async è fantastico, ma una sorta di async implicito non funzionerà nella realtà.

I linguaggi funzionali puri come Haskell hanno un po 'di tratteggio perché l'ordine di esecuzione è in gran parte non specificato e non osservabile. O espresso diversamente: qualsiasi ordine specifico di operazioni deve essere esplicitamente codificato. Questo può essere piuttosto ingombrante per i programmi del mondo reale, in particolare quei programmi pesanti di I / O per i quali il codice asincrono si adatta molto bene.


2
Non hai necessariamente bisogno di un sistema di tipi. Futuri trasparenti, ad esempio ECMAScript, Smalltalk, Self, Newspeak, Io, Ioke, Seph, possono essere facilmente implementati senza il sistema di te o il supporto linguistico. In Smalltalk e nei suoi discendenti, un oggetto può cambiare in modo trasparente la sua identità, in ECMAScript, può cambiare in modo trasparente la sua forma. Questo è tutto ciò di cui hai bisogno per rendere i Futures trasparenti, senza bisogno del supporto linguistico per l'asincronia.
Jörg W Mittag

6
@ JörgWMittag Capisco cosa stai dicendo e come potrebbe funzionare, ma i futures trasparenti senza un sistema di tipi rendono piuttosto difficile avere contemporaneamente futures di prima classe, no? Avrei bisogno di un modo per selezionare se desidero inviare messaggi al futuro o al valore del futuro, preferibilmente qualcosa di meglio rispetto al someValue ifItIsAFuture [self| self messageIWantToSend]fatto che è difficile integrarlo con il codice generico.
amon

8
@amon "Posso scrivere il mio codice asincrono poiché le promesse e le promesse sono monadi." Le monadi non sono effettivamente necessarie qui. I thunk sono essenzialmente solo promesse. Poiché quasi tutti i valori in Haskell sono inscatolati, quasi tutti i valori in Haskell sono già promesse. Ecco perché puoi lanciare parpraticamente ovunque nel puro codice Haskell e ottenere il paralellismo gratuitamente.
DarthFennec,

2
Async / waitit mi ricorda la monade di continuazione.
les

3
In effetti, entrambe le eccezioni e asincrono / attendono sono esempi di effetti algebrici .
Alex Reinking,

21

Quello che ti manca è lo scopo delle operazioni asincrone: ti permettono di sfruttare il tuo tempo di attesa!

Se trasformi un'operazione asincrona, come richiedere una risorsa da un server, in un'operazione sincrona aspettando implicitamente e immediatamente la risposta, il tuo thread non può fare nient'altro con il tempo di attesa . Se il server impiega 10 millisecondi per rispondere, si sprecano circa 30 milioni di cicli CPU. La latenza della risposta diventa il tempo di esecuzione della richiesta.

L'unico motivo per cui i programmatori hanno inventato operazioni asincrone è nascondere la latenza di attività intrinsecamente di lunga durata dietro altri calcoli utili . Se riesci a riempire il tempo di attesa con un lavoro utile, risparmia tempo sulla CPU. Se non puoi, beh, nulla è perso dall'operazione asincrona.

Quindi, raccomando di abbracciare le operazioni asincrone che le tue lingue ti forniscono. Sono lì per farti risparmiare tempo.


stavo pensando a un linguaggio funzionale in cui le operazioni non bloccano, quindi anche se ha una sintassi sincrona, un calcolo di lunga durata non bloccherà il thread
Cinn

6
@Cinn Non l'ho trovato nella domanda, e l'esempio nella domanda è Javascript, che non ha questa funzione. Tuttavia, in genere è piuttosto difficile per un compilatore trovare opportunità significative per la parallelizzazione mentre descrivi: lo sfruttamento significativo di tale funzionalità richiederebbe al programmatore di pensare esplicitamente a ciò che ha messo subito dopo una lunga chiamata di latenza. Se si rende il runtime abbastanza intelligente da evitare questo requisito per il programmatore, il runtime probabilmente consumerà i risparmi sulle prestazioni perché avrebbe bisogno di parallelizzare in modo aggressivo tra le chiamate di funzione.
cmaster

2
Tutti i computer attendono alla stessa velocità.
Bob Jarvis - Ripristina Monica

2
@BobJarvis Sì. Ma differiscono per quanto lavoro avrebbero potuto fare nel tempo di attesa ...
cmaster

13

Alcuni lo fanno.

Non sono mainstream (ancora) perché async è una funzionalità relativamente nuova per la quale abbiamo appena avuto una buona idea se è anche una buona funzionalità o come presentarla ai programmatori in un modo amichevole / utilizzabile / espressiva / etc. Le funzioni asincrone esistenti sono in gran parte collegate a linguaggi esistenti, che richiedono un approccio progettuale leggermente diverso.

Detto questo, non è chiaramente una buona idea fare ovunque. Un errore comune è l'esecuzione di chiamate asincrone in un ciclo, serializzando efficacemente la loro esecuzione. Avere implicite chiamate asincrone può oscurare questo tipo di errore. Inoltre, se supporti la coercizione implicita da un Task<T>(o l'equivalente della tua lingua) a T, ciò può aggiungere un po 'di complessità / costo al tuo typechecker e alla segnalazione degli errori quando non è chiaro quale dei due il programmatore desiderasse davvero.

Ma quelli non sono problemi insormontabili. Se volessi sostenere quel comportamento, quasi sicuramente potresti, anche se ci sarebbero dei compromessi.


1
Penso che un'idea potrebbe essere quella di avvolgere tutto in funzioni asincrone, i compiti sincroni si risolverebbero immediatamente e avremo un solo tipo da gestire (Modifica: @amon ha spiegato perché è una cattiva idea ...)
Cinn

8
Puoi fare qualche esempio per " Alcuni lo fanno ", per favore?
Bergi,

2
La programmazione asincrona non è affatto nuova, è solo che al giorno d'oggi le persone devono affrontarla più spesso.
Cubic

1
@Cubic: per quanto ne so, è una funzionalità linguistica. Prima erano solo funzioni (scomode) per l'utente.
Telastyn,

12

Ci sono lingue che lo fanno. Ma in realtà non c'è molto bisogno, dal momento che può essere facilmente realizzato con le funzionalità linguistiche esistenti.

Finché hai un modo per esprimere l'asincronia, puoi implementare Futures o Promesse esclusivamente come una funzione di libreria, non hai bisogno di funzioni linguistiche speciali. E fintanto che hai un po ' di esprimere i proxy trasparenti , puoi mettere insieme le due caratteristiche e avere Future trasparenti .

Ad esempio, in Smalltalk e nei suoi discendenti, un oggetto può cambiare la sua identità, può letteralmente "diventare" un oggetto diverso (e in effetti viene chiamato il metodo che lo fa Object>>become:).

Immagina un calcolo di lunga durata che restituisce a Future<Int>. Questo Future<Int>ha tutti gli stessi metodi di Int, tranne con diverse implementazioni. Future<Int>Il +metodo non aggiunge un altro numero e restituisce il risultato, restituisce un nuovo Future<Int>che avvolge il calcolo. E così via e così via. Metodi che non può ragionevolmente essere attuate restituendo un Future<Int>, sarà invece automaticamente awaitil risultato, e quindi chiamare self become: result., che renderà l'oggetto attualmente in esecuzione ( selfcioè la Future<Int>) letteralmente diventare l' resultoggetto, cioè da ora in poi il riferimento all'oggetto che era un Future<Int>è ora un Intovunque, completamente trasparente per il cliente.

Non sono necessarie funzioni linguistiche speciali correlate all'asincronia.


Ok, ma questo ha problemi se entrambi Future<T>e Tcondivido un'interfaccia comune e io utilizzo funzionalità da quella interfaccia. Dovrebbe becomeil risultato e quindi utilizzare la funzionalità o no? Sto pensando a cose come un operatore di uguaglianza o una rappresentazione di debug su stringa.
amon

Capisco che non aggiunge alcuna funzionalità, il fatto è che abbiamo diverse sintassi per scrivere calcoli risolutivi immediati e calcoli di lunga durata, e successivamente utilizzeremo i risultati allo stesso modo per altri scopi. Mi chiedevo se avremmo potuto avere una sintassi che gestisse in modo trasparente entrambi, rendendolo più leggibile e quindi il programmatore non deve gestirlo. Come fare a + b, entrambi numeri interi, non importa se a e b sono disponibili immediatamente o successivamente, scriviamo a + b(rendendo possibile farlo Int + Future<Int>)
Cinn

@Cinn: Sì, puoi farlo con Transparent Futures e non hai bisogno di funzioni linguistiche speciali per farlo. Puoi implementarlo usando le funzionalità già esistenti in es. Smalltalk, Self, Newspeak, Us, Korz, Io, Ioke, Seph, ECMAScript e apparentemente, come ho appena letto, Python.
Jörg W Mittag

3
@amon: L'idea di Transparent Futures è che non sai che è un futuro. Dal tuo punto di vista, non esiste un'interfaccia comune tra Future<T>e Tperché dal tuo punto di vista, non esisteFuture<T> , solo a T. Ora, ovviamente, ci sono molte sfide ingegneristiche su come renderlo efficiente, quali operazioni dovrebbero essere bloccate contro non bloccanti, ecc., Ma questo è davvero indipendente dal fatto che lo si faccia come una lingua o come una funzione di libreria. La trasparenza era un requisito stabilito dall'OP nella domanda, non sosterrò che è difficile e potrebbe non avere senso.
Jörg W Mittag,

3
@ Jörg Sembra che sarebbe problematico in tutto tranne che nei linguaggi funzionali poiché non hai modo di sapere quando il codice viene effettivamente eseguito in quel modello. Questo in genere funziona bene nel dire Haskell, ma non riesco a vedere come funzionerebbe in più linguaggi procedurali (e anche in Haskell, se ti preoccupi delle prestazioni a volte devi forzare un'esecuzione e capire il modello sottostante). Un'idea interessante comunque.
Voo

7

Lo fanno (bene, la maggior parte di loro). La funzione che stai cercando si chiama thread .

Le discussioni hanno i loro problemi però:

  1. Poiché il codice può essere sospeso in qualsiasi momento , non si può mai presumere che le cose non cambieranno "da sole". Quando programmate con i thread, perdete molto tempo a pensare a come il vostro programma dovrebbe affrontare le cose che cambiano.

    Immagina che un server di gioco stia elaborando l'attacco di un giocatore su un altro giocatore. Qualcosa come questo:

    if (playerInMeleeRange(attacker, victim)) {
        const damage = calculateAttackDamage(attacker, victim);
        if (victim.health <= damage) {
    
            // attacker gets whatever the victim was carrying as loot
            const loot = victim.getInventoryItems();
            attacker.addInventoryItems(loot);
            victim.removeInventoryItems(loot);
    
            victim.sendMessage("${attacker} hits you with a ${attacker.currentWeapon} and you die!");
            victim.setDead();
        } else {
            victim.health -= damage;
            victim.sendMessage("${attacker} hits you with a ${attacker.currentWeapon}!");
        }
        attacker.markAsKiller();
    }
    

    Tre mesi dopo, un giocatore scopre che uccidendosi e disconnettendosi esattamente quando attacker.addInventoryItemsè in esecuzione, quindi victim.removeInventoryItemsfallirà, può conservare i suoi oggetti e l'attaccante ottiene anche una copia dei suoi oggetti. Lo fa più volte, creando un milione di tonnellate di oro dal nulla e facendo crollare l'economia del gioco.

    In alternativa, l'attaccante può disconnettersi mentre il gioco sta inviando un messaggio alla vittima e non otterrà un tag "assassino" sopra la sua testa, quindi la sua prossima vittima non scapperà da lui.

  2. Poiché il codice può essere sospeso in qualsiasi momento , è necessario utilizzare i blocchi ovunque per manipolare le strutture di dati. Ho dato un esempio sopra che ha ovvie conseguenze in un gioco, ma può essere più sottile. Valuta di aggiungere un elemento all'inizio di un elenco collegato:

    newItem.nextItem = list.firstItem;
    list.firstItem = newItem;
    

    Questo non è un problema se dici che i thread possono essere sospesi solo quando stanno eseguendo I / O e non in nessun momento. Ma sono sicuro che puoi immaginare una situazione in cui è presente un'operazione di I / O, come la registrazione:

    for (player = playerList.firstItem; player != null; player = item.nextPlayer) {
        debugLog("${item.name} is online, they get a gold star");
        // Oops! The player might've logged out while the log message was being written to disk, and now this will throw an exception and the remaining players won't get their gold stars.
        // Or the list might've been rearranged and some players might get two and some players might get none.
        player.addInventoryItem(InventoryItems.GoldStar);
    }
    
  3. Poiché il codice può essere sospeso in qualsiasi momento , potrebbe esserci potenzialmente molto stato da salvare. Il sistema si occupa di questo dando ad ogni thread uno stack completamente separato. Ma lo stack è piuttosto grande, quindi non puoi avere più di circa 2000 thread in un programma a 32 bit. Oppure potresti ridurre le dimensioni dello stack, a rischio di renderlo troppo piccolo.


3

Molte risposte qui sono fuorvianti, perché mentre la domanda stava letteralmente ponendo domande sulla programmazione asincrona e non sull'IO non bloccante, non penso che possiamo discutere l'una senza discutere l'altra in questo caso particolare.

Mentre la programmazione asincrona è intrinsecamente, beh, asincrona, la ragion d'essere della programmazione asincrona è principalmente quella di evitare il blocco dei thread del kernel. Node.js utilizza l'asincronismo tramite callback o Promises per consentire l'invio di operazioni di blocco da un loop di eventi e Netty in Java utilizza l'asincronismo tramite callback o CompletableFutures per fare qualcosa di simile.

Tuttavia, il codice non bloccante non richiede asincronizzazione . Dipende da quanto il tuo linguaggio di programmazione e il tuo runtime sono disposti a fare per te.

Go, Erlang e Haskell / GHC possono gestirlo per te. Puoi scrivere qualcosa del genere var response = http.get('example.com/test')e farlo rilasciare un thread del kernel dietro le quinte mentre aspetti una risposta. Questo viene fatto da goroutine, processi di Erlang o forkIOlasciando andare i thread del kernel dietro le quinte durante il blocco, permettendogli di fare altre cose in attesa di una risposta.

È vero che il linguaggio non può davvero gestire l'asincronicità per te, ma alcune astrazioni ti consentono di andare oltre rispetto ad altri, ad esempio continuazioni non delimitate o coroutine asimmetriche. Tuttavia, la causa principale del codice asincrono, il blocco delle chiamate di sistema, può essere assolutamente sottratta allo sviluppatore.

Node.js e Java supportano il codice asincrono non bloccante , mentre Go ed Erlang supportano il codice sincrono non bloccante . Sono entrambi approcci validi con diversi compromessi.

La mia argomentazione piuttosto soggettiva è che coloro che litigano contro i tempi di autonomia che gestiscono il blocco non per conto dello sviluppatore sono come quelli che discutono contro la raccolta dei rifiuti nei primi anni '40. Sì, comporta un costo (in questo caso principalmente più memoria), ma semplifica lo sviluppo e il debug e rende più robuste le basi di codice.

Personalmente sosterrei che in futuro il codice asincrono non bloccante dovrebbe essere riservato alla programmazione di sistemi e che stack di tecnologie più moderne dovrebbero migrare verso runtime sincroni non bloccanti per lo sviluppo di applicazioni.


1
Questa è stata una risposta davvero interessante! Ma non sono sicuro di comprendere la tua distinzione tra codice non bloccante "sincrono" e "asincrono". Per me, il codice sincrono non bloccante significa che qualcosa come una funzione C come waitpid(..., WNOHANG)quella fallisce se dovesse bloccarsi . Oppure "sincrono" qui significa "non ci sono callback / macchine a stati / loop di eventi visibili al programmatore"? Ma per il tuo esempio Go, devo ancora attendere esplicitamente un risultato da una goroutine leggendo da un canale, no? In che modo questo è meno asincrono di asincrono / wait in JS / C # / Python?
amon

1
Uso "asincrono" e "sincrono" per discutere il modello di programmazione esposto allo sviluppatore e "blocco" e "non blocco" per discutere il blocco di un thread del kernel durante il quale non può fare nulla di utile, anche se ci sono altri calcoli che devono essere eseguiti e esiste un processore logico di riserva che può utilizzare. Bene, una goroutine può semplicemente aspettare un risultato senza bloccare il thread sottostante, ma un'altra goroutine può comunicare con esso su un canale se lo desidera. Tuttavia, la goroutine non deve utilizzare direttamente un canale per attendere la lettura di un socket non bloccante.
Louis Jackman,

Hmm ok, ora capisco la tua distinzione. Mentre sono più preoccupato per la gestione del flusso di dati e di controllo tra le coroutine, sei più preoccupato di non bloccare mai il thread principale del kernel. Non sono sicuro che Go o Haskell abbiano qualche vantaggio rispetto a C ++ o Java in questo senso, poiché anche loro possono dare il via ai thread in background, per farlo richiede solo un po 'più di codice.
amon

@LouisJackman potrebbe elaborare un po 'la tua ultima affermazione sul non-blocco asincrono per la programmazione del sistema. Quali sono i vantaggi dell'approccio asincrono non bloccante?
sunprophit l'

@sunprophit Il non-blocco asincrono è solo una trasformazione del compilatore (di solito asincrono / attendono), mentre il non-blocco sincrono richiede il supporto del runtime come una combinazione di manipolazione complessa dello stack, inserendo punti di snervamento nelle chiamate di funzione (che possono scontrarsi con inline), monitorando " riduzioni ”(che richiedono una macchina virtuale come BEAM), ecc. Come la garbage collection, si traduce in una minore complessità di runtime per facilità d'uso e robustezza. Linguaggi di sistema come C, C ++ e Rust evitano funzionalità di runtime più grandi come questa a causa dei loro domini target, quindi il blocco asincrono non ha più senso.
Louis Jackman,

2

Se ti sto leggendo bene, stai chiedendo un modello di programmazione sincrona, ma un'implementazione ad alte prestazioni. Se è corretto, allora è già a nostra disposizione sotto forma di fili verdi o processi come ad esempio Erlang o Haskell. Quindi sì, è un'idea eccellente, ma il retrofit per le lingue esistenti non può sempre essere fluido come vorresti.


2

Apprezzo la domanda e trovo che la maggior parte delle risposte sia semplicemente difensiva dello status quo. Nello spettro delle lingue da basso ad alto livello, siamo rimasti bloccati in una carreggiata per qualche tempo. Il prossimo livello superiore sarà chiaramente un linguaggio meno focalizzato sulla sintassi (la necessità di parole chiave esplicite come wait e async) e molto di più sull'intenzione. (Evidente merito a Charles Simonyi, ma pensando al 2019 e al futuro.)

Se lo dicessi a un programmatore, scrivi un codice che recupera semplicemente un valore da un database, puoi tranquillamente supporre che intendo "e BTW, non appendere l'interfaccia utente" e "non introdurre altre considerazioni che mascherano i bug difficili da trovare ". I programmatori del futuro, con una prossima generazione di linguaggi e strumenti, saranno sicuramente in grado di scrivere codice che recupera semplicemente un valore in una riga di codice e va da lì.

La lingua di livello più elevato sarebbe parlare inglese e fare affidamento sulla competenza del responsabile per sapere cosa vuoi veramente fare. (Pensa al computer in Star Trek, o chiedi qualcosa ad Alexa.) Siamo lontani da questo, ma ci avviciniamo sempre più, e la mia aspettativa è che il linguaggio / compilatore potrebbe essere più in grado di generare un codice robusto e intenzionale senza andare così lontano da bisogno di intelligenza artificiale.

Da un lato, ci sono linguaggi visivi più recenti, come Scratch, che fanno questo e non sono impantanati con tutti i tecnicismi sintattici. Certamente, ci sono molti lavori dietro le quinte in corso, quindi il programmatore non deve preoccuparsene. Detto questo, non sto scrivendo software di classe business in Scratch, quindi, come te, ho la stessa aspettativa che sia tempo che i linguaggi di programmazione maturi gestiscano automaticamente il problema sincrono / asincrono.


1

Il problema che stai descrivendo è duplice.

  • Il programma che stai scrivendo dovrebbe comportarsi in modo asincrono nel suo insieme se visto dall'esterno .
  • Dovrebbe non essere visibile sul sito chiamata se una chiamata di funzione fornisce potenzialmente il controllo o meno.

Ci sono un paio di modi per raggiungere questo obiettivo, ma sostanzialmente si riducono a

  1. avere più thread (a un certo livello di astrazione)
  2. avere più tipi di funzioni a livello linguistico, tutte chiamate in questo modo foo(4, 7, bar, quux).

Per (1), sto raggruppando insieme il fork e l'esecuzione di più processi, generando più thread del kernel e implementazioni di thread verdi che pianificano thread a livello di runtime di linguaggio sui thread del kernel. Dal punto di vista del problema, sono gli stessi. In questo mondo, nessuna funzione si arrende mai o perde il controllo dalla prospettiva del suo thread . Il thread stesso a volte non ha controllo e talvolta non è in esecuzione ma non si rinuncia al controllo del proprio thread in questo mondo. Un sistema adatto a questo modello può avere o meno la possibilità di generare nuovi thread o unirsi a thread esistenti. Un sistema adatto a questo modello può avere o meno la possibilità di duplicare un thread come quello di Unix fork.

(2) è interessante. Per rendere giustizia dobbiamo parlare di moduli di introduzione ed eliminazione.

Mostrerò perché awaitnon è possibile aggiungere implicito a una lingua come Javascript in modo compatibile con le versioni precedenti. L'idea di base è che esponendo le promesse all'utente e distinguendo tra contesti sincroni e asincroni, Javascript ha fatto trapelare un dettaglio di implementazione che impedisce la gestione uniforme delle funzioni sincrone e asincrone. C'è anche il fatto che non puoi awaitpromettere al di fuori di un corpo di funzione asincrono. Queste scelte progettuali sono incompatibili con "rendere invisibile l'asincrono al chiamante".

È possibile introdurre una funzione sincrona utilizzando una lambda ed eliminarla con una chiamata di funzione.

Introduzione alla funzione sincrona:

((x) => {return x + x;})

Eliminazione della funzione sincrona:

f(4)

((x) => {return x + x;})(4)

È possibile contrastare questo con l'introduzione e l'eliminazione della funzione asincrona.

Introduzione alla funzione asincrona

(async (x) => {return x + x;})

Eliminazione della funzione asincrona (nota: valida solo all'interno di una asyncfunzione)

await (async (x) => {return x + x;})(4)

Il problema fondamentale qui è che una funzione asincrona è anche una funzione sincrona che produce un oggetto promessa .

Ecco un esempio di come chiamare una funzione asincrona in modo sincrono nella sostituzione node.js.

> (async (x) => {return x + x;})(4)
Promise { 8 }

È possibile ipotizzare un linguaggio, anche se digitato in modo dinamico, in cui la differenza tra chiamate di funzione asincrone e sincrone non è visibile nel sito di chiamata e probabilmente non è visibile nel sito di definizione.

Prendendo una lingua del genere e abbassandola a Javascript è possibile, dovresti semplicemente rendere tutte le funzioni asincrone.


1

Con le goroutine di lingua Go e il tempo di esecuzione della lingua Go, puoi scrivere tutto il codice come se fosse sincrone. Se un'operazione si blocca in una goroutine, l'esecuzione continua in altre goroutine. E con i canali puoi comunicare facilmente tra le goroutine. Questo è spesso più semplice dei callback come in Javascript o asincroni / attendi in altre lingue. Vedere https://tour.golang.org/concurrency/1 per alcuni esempi e una spiegazione.

Inoltre, non ho esperienza personale, ma sento che Erlang ha strutture simili.

Quindi sì, ci sono linguaggi di programmazione come Go ed Erlang, che risolvono il problema sincrono / asincrono, ma sfortunatamente non sono ancora molto popolari. Man mano che queste lingue crescono in popolarità, forse le strutture che forniscono saranno implementate anche in altre lingue.


Non ho quasi mai usato la lingua Go ma sembra che tu dichiari esplicitamente go ..., quindi sembra simile come await ...no?
Cinn,

1
@Cinn In realtà, no. È possibile effettuare qualsiasi chiamata come goroutine sul proprio thread in fibra / verde con go. E quasi ogni chiamata che potrebbe bloccare viene eseguita in modo asincrono dal runtime, che nel frattempo passa a una goroutine diversa (multi-tasking cooperativo). Aspetti aspettando un messaggio.
Deduplicatore

2
Mentre le Goroutine sono una specie di concorrenza, non le metterei nello stesso secchio di Asinc / Attendi: non coroutine cooperative ma fili verdi programmati automaticamente (e preventivamente!). Ma questo non rende neanche automatico l'attesa: Go equivale a awaitleggere da un canale <- ch.
amon

@amon Per quanto ne so, le goroutine sono pianificate in modo cooperativo su thread nativi (normalmente quanto basta per massimizzare il vero parallelismo hardware) dal runtime, e quelle sono preventivamente programmate dal sistema operativo.
Deduplicatore

L'OP ha chiesto "di essere in grado di scrivere codice asincrono in modo sincrono". Come hai già detto, con le goroutine e il runtime di go, puoi esattamente farlo. Non devi preoccuparti dei dettagli del threading, basta scrivere letture e scritture di blocco, come se il codice fosse sincrono e le altre tue goroutine continueranno a funzionare. Inoltre non devi nemmeno "aspettare" o leggere da un canale per ottenere questo vantaggio. Penso quindi che Go stia programmando un linguaggio che soddisfa i desideri del PO più da vicino.

1

C'è un aspetto molto importante che non è stato ancora sollevato: rientro. Se hai qualche altro codice (es .: loop di eventi) che viene eseguito durante la chiamata asincrona (e se non lo fai, perché hai anche bisogno di asincrono?), Allora il codice può influenzare lo stato del programma. Non è possibile nascondere le chiamate asincrone dal chiamante perché il chiamante può dipendere da parti dello stato del programma per rimanere inalterato per la durata della sua chiamata di funzione. Esempio:

function foo( obj ) {
    obj.x = 2;
    bar();
    log( "obj.x equals 2: " + obj.x );
}

Se bar()è una funzione asincrona, potrebbe essere possibile obj.xcambiare durante la sua esecuzione. Ciò sarebbe piuttosto inaspettato senza alcun suggerimento che la barra sia asincrona e che l'effetto sia possibile. L'unica alternativa sarebbe sospettare che ogni possibile funzione / metodo sia asincrono e recuperare nuovamente e ricontrollare qualsiasi stato non locale dopo ogni chiamata di funzione. Questo è soggetto a bug sottili e potrebbe non essere nemmeno possibile se alcuni stati non locali vengono recuperati tramite funzioni. Per questo motivo, il programmatore deve essere consapevole di quale delle funzioni ha il potenziale di alterare lo stato del programma in modi inaspettati:

async function foo( obj ) {
    obj.x = 2;
    await bar();
    log( "obj.x equals 2: " + obj.x );
}

Ora è chiaramente visibile che si bar()tratta di una funzione asincrona e il modo corretto di gestirla è ricontrollare il valore atteso di obj.xseguito e gestire eventuali cambiamenti che potrebbero essersi verificati.

Come già notato da altre risposte, i linguaggi puramente funzionali come Haskell possono sfuggire completamente a questo effetto evitando la necessità di qualsiasi stato condiviso / globale. Non ho molta esperienza con i linguaggi funzionali, quindi probabilmente sono di parte, ma non credo che la mancanza dello stato globale sia un vantaggio quando si scrivono applicazioni più grandi.


0

Nel caso di Javascript, che hai usato nella tua domanda, c'è un punto importante da tenere presente: Javascript è a thread singolo e l'ordine di esecuzione è garantito fintanto che non ci sono chiamate asincrone.

Quindi se hai una sequenza come la tua:

const nbOfUsers = getNbOfUsers();

Sei garantito che nel frattempo non verrà eseguito nient'altro. Non c'è bisogno di lucchetti o qualcosa di simile.

Tuttavia, se getNbOfUsersè asincrono, quindi:

const nbOfUsers = await getNbOfUsers();

significa che mentre getNbOfUsersè in esecuzione, i rendimenti di esecuzione e altri codici possono essere eseguiti in mezzo. Questo a sua volta potrebbe richiedere un certo blocco, a seconda di cosa stai facendo.

Quindi, è una buona idea essere consapevoli quando una chiamata è asincrona e quando non lo è, poiché in alcune situazioni è necessario prendere ulteriori precauzioni che non sarebbe necessario se la chiamata fosse sincrona.


Hai ragione, il mio secondo codice nella domanda non è valido come se getNbOfUsers()restituisse una Promessa. Ma questo è esattamente il punto della mia domanda, perché dobbiamo scriverlo esplicitamente come asincrono, il compilatore potrebbe rilevarlo e gestirlo automaticamente in un modo diverso.
Cinn

@Cinn non è questo il mio punto. Il mio punto è che il flusso di esecuzione potrebbe arrivare ad altre parti del codice durante l'esecuzione della chiamata asincrona, mentre non è possibile per una chiamata sincrona. Sarebbe come avere più thread in esecuzione ma non esserne consapevoli. Questo può finire in grossi problemi (che di solito sono difficili da rilevare e riprodurre).
Jcaron

-4

Questo è disponibile in C ++ come std::asyncda C ++ 11.

La funzione template async esegue la funzione f in modo asincrono (potenzialmente in un thread separato che può far parte di un pool di thread) e restituisce uno std :: future che alla fine conterrà il risultato di quella chiamata di funzione.

E con C ++ 20 coroutine si possono usare:


5
Questo non sembra rispondere alla domanda. Secondo il tuo link: "Cosa ci fornisce la Coroutines TS? Tre parole chiave in una nuova lingua: co_await, co_yield e co_return" ... Ma la domanda è: perché abbiamo bisogno di una await(o co_awaitin questo caso) parola chiave in primo luogo?
Arturo Torres Sánchez,
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.