Perché le coroutine sono tornate? [chiuso]


19

La maggior parte delle basi per le coroutine avvenne negli anni '60 / '70 e poi si fermò a favore di alternative (ad es. Discussioni)

C'è qualche sostanza nel rinnovato interesse per le coroutine che si è verificato in Python e in altre lingue?



9
Non sono sicuro che se ne siano mai andati.
Blrfl,

Risposte:


26

Le coroutine non se ne sono mai andate, nel frattempo sono state semplicemente oscurate da altre cose. Il recente crescente interesse per la programmazione asincrona e quindi le coroutine è in gran parte dovuto a tre fattori: maggiore accettazione delle tecniche di programmazione funzionale, set di strumenti con scarso supporto per il vero parallelismo (JavaScript! Python!) E, soprattutto: i diversi compromessi tra thread e coroutine. Per alcuni casi d'uso, le coroutine sono oggettivamente migliori.

Uno dei più grandi paradigmi di programmazione degli anni '80, '90 e oggi è OOP. Se guardiamo alla storia di OOP e in particolare allo sviluppo del linguaggio Simula, vediamo che le classi si sono evolute dalle coroutine. Simula era destinato alla simulazione di sistemi con eventi discreti. Ogni elemento del sistema era un processo separato che si sarebbe eseguito in risposta agli eventi per la durata di una fase di simulazione, per poi cedere alle altre attività che avrebbero fatto il loro lavoro. Durante lo sviluppo di Simula 67 è stato introdotto il concetto di classe. Ora lo stato persistente della routine viene memorizzato nei membri dell'oggetto e gli eventi vengono attivati ​​chiamando un metodo. Per maggiori dettagli, considera la lettura del documento Lo sviluppo delle lingue SIMULA di Nygaard & Dahl.

Quindi, in un modo divertente, abbiamo sempre usato le coroutine, le chiamavamo semplicemente oggetti e programmazione guidata dagli eventi.

Per quanto riguarda il parallelismo, esistono due tipi di linguaggi: quelli che hanno un modello di memoria adeguato e quelli che non lo sono. Un modello di memoria discute cose come “Se scrivo su una variabile e dopo che ho letto da quella variabile in un altro thread, vedo il vecchio valore o il nuovo valore o forse un valore non valido? Cosa significano "prima" e "dopo"? Quali operazioni sono garantite per essere atomiche? "

Creare un buon modello di memoria è difficile, quindi questo sforzo non è mai stato fatto per la maggior parte di questi linguaggi open source dinamici non specificati e definiti dall'implementazione: Perl, JavaScript, Python, Ruby, PHP. Naturalmente, tutte quelle lingue si sono evolute ben oltre lo "scripting" per cui erano state originariamente costruite. Bene, alcune di queste lingue hanno una sorta di documento modello di memoria, ma quelli non sono sufficienti. Invece, abbiamo hack:

  • Perl può essere compilato con il supporto di threading, ma ogni thread contiene un clone separato dello stato dell'interprete completo, rendendo i thread proibitivamente costosi. Come unico vantaggio, questo approccio al nulla condiviso evita le gare di dati e costringe i programmatori a comunicare solo attraverso code / segnali / IPC. Perl non ha una storia forte per l'elaborazione asincrona.

  • JavaScript ha sempre avuto un ampio supporto per la programmazione funzionale, quindi i programmatori codificavano manualmente continuazioni / callback nei loro programmi dove avevano bisogno di operazioni asincrone. Ad esempio, con richieste Ajax o ritardi nell'animazione. Poiché il Web è intrinsecamente asincrono, c'è molto codice JavaScript asincrono e gestire tutti questi callback è immensamente doloroso. Vediamo quindi molti sforzi per organizzare meglio quei callback (Promesse) o per eliminarli del tutto.

  • Python ha questa sfortunata funzionalità chiamata Global Interpreter Lock. Fondamentalmente il modello di memoria di Python è “Tutti gli effetti appaiono in sequenza perché non c'è parallelismo. Solo un thread eseguirà il codice Python alla volta. ”Quindi mentre Python ha dei thread, questi sono potenti quanto le coroutine. [1] Python può codificare molte coroutine tramite le funzioni del generatore con yield. Se usato correttamente, questo da solo può evitare la maggior parte dell'inferno di callback conosciuto da JavaScript. Il più recente sistema asincrono / attesa di Python 3.5 rende i linguaggi asincroni più convenienti in Python e integra un ciclo di eventi.

    [1]: Tecnicamente queste restrizioni si applicano solo a CPython, l'implementazione di riferimento di Python. Altre implementazioni come Jython offrono thread reali che possono essere eseguiti in parallelo, ma devono fare molto per implementare comportamenti equivalenti. In sostanza: ogni variabile o membro dell'oggetto è una variabile volatile in modo che tutte le modifiche siano atomiche e vengano immediatamente visualizzate in tutti i thread. Naturalmente, l'uso di variabili volatili è molto più costoso rispetto all'utilizzo di variabili normali.

  • Non so abbastanza su Ruby e PHP per arrostirli correttamente.

Riassumendo: alcuni di questi linguaggi hanno decisioni progettuali fondamentali che rendono indesiderabile o impossibile il multithreading, portando a una maggiore attenzione alle alternative come le coroutine e ai modi per rendere più conveniente la programmazione asincrona.

Infine, parliamo delle differenze tra coroutine e fili:

I thread sono fondamentalmente come processi, tranne per il fatto che più thread all'interno di un processo condividono uno spazio di memoria. Ciò significa che i thread non sono affatto “leggeri” in termini di memoria. Le discussioni sono programmate preventivamente dal sistema operativo. Ciò significa che i task switch hanno un elevato sovraccarico e possono verificarsi in momenti non convenienti. Questo sovraccarico ha due componenti: il costo della sospensione dello stato del thread e il costo del passaggio dalla modalità utente (per il thread) alla modalità kernel (per lo scheduler).

Se un processo pianifica i propri thread direttamente e in modo cooperativo, il passaggio dal contesto alla modalità kernel non è necessario e il passaggio da un compito all'altro è relativamente costoso per una chiamata di funzione indiretta, come in: abbastanza economico. Questi fili leggeri possono essere chiamati fili verdi, fibre o coroutine a seconda di vari dettagli. Notevoli utenti di fili / fibre verdi erano le prime implementazioni di Java, e più recentemente Goroutine a Golang. Un vantaggio concettuale delle coroutine è che la loro esecuzione può essere compresa in termini di flusso di controllo che passa esplicitamente avanti e indietro tra le coroutine. Tuttavia, queste coroutine non ottengono un vero parallelismo a meno che non siano pianificate su più thread del sistema operativo.

Dove sono utili le coroutine economiche? La maggior parte dei software non ha bisogno di un thread di gazillion, quindi i normali thread costosi sono generalmente OK. Tuttavia, la programmazione asincrona a volte può semplificare il codice. Per essere usata liberamente, questa astrazione deve essere sufficientemente economica.

E poi c'è il web. Come accennato in precedenza, il Web è intrinsecamente asincrono. Le richieste di rete richiedono semplicemente molto tempo. Molti server Web mantengono un pool di thread pieno di thread di lavoro. Tuttavia, la maggior parte del tempo questi thread resteranno inattivi perché sono in attesa di alcune risorse, sia che si tratti di un evento I / O durante il caricamento di un file dal disco, sia in attesa che il client abbia riconosciuto parte della risposta o in attesa di un database la query è completata. NodeJS ha dimostrato in modo fenomenale che una conseguente progettazione di server basata su eventi e asincrona funziona estremamente bene. Ovviamente JavaScript è tutt'altro che l'unico linguaggio utilizzato per le applicazioni web, quindi c'è anche un grande incentivo per altri linguaggi (evidente in Python e C #) per facilitare la programmazione web asincrona.


Consiglierei di procurarti il ​​tuo quarto e ultimo paragrafo per evitare il rischio di plagio, è quasi lo stesso di un'altra fonte che ho letto. Inoltre, pur avendo ordini di grandezza minori rispetto ai thread, le prestazioni delle coroutine non possono essere semplificate in "una chiamata di funzione indiretta". Vedi i dettagli di Boosts sulle implementazioni coroutine qui e qui .
quando

1
@snb Per quanto riguarda la modifica suggerita: GIL potrebbe essere un dettaglio dell'implementazione di CPython, ma il problema fondamentale è che il linguaggio Python non ha un modello di memoria esplicito che specifica la mutazione parallela dei dati. Il GIL è un trucco per eludere questi problemi. Ma le implementazioni di Python con vero parallelismo devono fare di tutto per fornire una semantica equivalente, ad esempio come discusso nel libro di Jython . Fondamentalmente: ogni variabile o campo oggetto deve essere una variabile volatile costosa .
amon

3
@snb Riguardo al plagio: il plagio presenta erroneamente idee come tue, specialmente in un contesto accademico. È un'affermazione seria , ma sono sicuro che non lo intendevi in ​​quel modo. Il paragrafo "I thread sono sostanzialmente come i processi" reiterano semplicemente fatti ben noti come viene insegnato in qualsiasi lezione o libro di testo sui sistemi operativi. Dal momento che ci sono solo tanti modi per esprimere concisamente questi fatti, non mi sorprende che il paragrafo ti sia familiare.
amon

Non ho cambiato il senso di implicare che Python ha fatto avere un modello di memoria. Inoltre, l'uso di volatile non riduce di per sé le prestazioni volatili semplicemente significa che il compilatore non può ottimizzare la variabile in un modo in cui può presumere che la variabile rimarrà invariata senza operazioni esplicite nel contesto attuale. Nel mondo Jython questo potrebbe davvero importare, dal momento che utilizzerà la compilazione VM JIT, ma nel mondo CPython non ti preoccupi dell'ottimizzazione JIT, le tue variabili volatili esisterebbero nello spazio di runtime dell'interprete, dove non potrebbero essere fatte ottimizzazioni .
quando l'

7

Le coroutine erano utili perché i sistemi operativi non eseguivano la pianificazione preventiva . Una volta che hanno iniziato a fornire la pianificazione preventiva, era più necessario rinunciare periodicamente al controllo nel programma.

Man mano che i processori multi-core diventano sempre più diffusi, le coroutine vengono utilizzate per ottenere il parallelismo delle attività e / o mantenere elevato l'utilizzo di un sistema (quando un thread di esecuzione deve attendere una risorsa, un altro può iniziare a funzionare al suo posto).

NodeJS è un caso speciale, in cui vengono utilizzate le coroutine per accedere in parallelo a IO. Cioè, più thread vengono utilizzati per soddisfare le richieste IO, ma un singolo thread viene utilizzato per eseguire il codice javascript. Lo scopo di eseguire un codice utente in un thread signle è quello di evitare la necessità di usare i mutex. Questo rientra nella categoria del tentativo di mantenere alto l'utilizzo del sistema come menzionato sopra.


4
Ma le coroutine non sono gestite dal SO. Il sistema operativo non sa cosa sia una coroutine, a differenza delle fibre di C ++
scambio eccessivo del

Molti sistemi operativi hanno coroutine.
Jörg W Mittag,

coroutine come python e Javascript ES6 + non sono però multiprocesso? In che modo ottengono il parallelismo dei compiti?
quando

1
@Mael Il recente "risveglio" delle coroutine proviene da pitone e javascript, entrambi i quali non ottengono parallelismo con le loro coroutine a quanto ho capito. Ciò significa che questa risposta non è corretta, poiché il pappagallismo non è la ragione per cui le coroutine sono "tornate". Anche Luas non è multiprocesso? EDIT: Ho appena realizzato che non stavi parlando di parallelismo, ma perché mi hai risposto in primo luogo? Rispondi a dlasalle, poiché chiaramente si sbagliano su questo.
quando

3
@dlasalle No, non possono farlo nonostante il fatto che si dice "in esecuzione in parrallel" che non significa che nessun codice sia eseguito fisicamente allo stesso tempo. GIL lo fermerebbe e asincrono non genererebbe processi separati richiesti per il multiprocessing in CPython (GIL separati). Async funziona con i rendimenti su un singolo thread. Quando dicono "parralel", in realtà significano diverse funzioni che danno origine ad altre funzioni e che eseguono l' esecuzione di funzioni interleving . I processi asincroni di Python non possono essere eseguiti in parallelo a causa di impl. Ora ho tre lingue che non fanno coroutine parralel, Lua, Javascript e Python.
quando

5

I primi sistemi utilizzavano le coroutine per fornire concorrenza principalmente perché sono il modo più semplice di farlo. I thread richiedono un discreto supporto dal sistema operativo (puoi implementarli a livello di utente, ma avrai bisogno di un modo per organizzare il sistema per interrompere periodicamente il tuo processo) e sono più difficili da implementare anche quando hai il supporto .

I thread hanno iniziato a prendere il controllo in seguito perché, negli anni '70 o '80, tutti i sistemi operativi seri li supportavano (e, negli anni '90, anche Windows!), E sono più generici. E sono più facili da usare. All'improvviso tutti pensarono che i fili fossero la prossima grande cosa.

Alla fine degli anni '90 iniziarono ad apparire crepe e nei primi anni 2000 divenne evidente che c'erano seri problemi con i thread:

  1. consumano molte risorse
  2. i cambi di contesto richiedono molto tempo, relativamente parlando, e spesso non sono necessari
  3. distruggono la località di riferimento
  4. scrivere codice corretto che coordina più risorse che potrebbero richiedere un accesso esclusivo è inaspettatamente difficile

Nel corso del tempo, il numero di attività che i programmi in genere devono svolgere in qualsiasi momento è cresciuto rapidamente, aumentando i problemi causati da (1) e (2) sopra. La disparità tra la velocità del processore e i tempi di accesso alla memoria è aumentata, aggravando il problema (3). E la complessità dei programmi in termini di quante e quali diversi tipi di risorse richiedono è cresciuta, aumentando la rilevanza del problema (4).

Ma perdendo un po 'di generalità e mettendo un po' di onere in più sul programmatore per pensare a come i loro processi possono funzionare insieme, le coroutine possono risolvere tutti questi problemi.

  1. Le coroutine richiedono un po 'più di risorse rispetto a una manciata di pagine da impilare, molto meno della maggior parte delle implementazioni di thread.
  2. Le coroutine cambiano contesto solo nei punti definiti dal programmatore, il che si spera significhi solo quando è necessario. Inoltre, di solito non hanno bisogno di conservare quante più informazioni sul contesto (es. Valori di registro) come fanno i thread, il che significa che ogni switch è di solito più veloce e ne ha bisogno di meno.
  3. I modelli comuni di coroutine, comprese le operazioni di tipo produttore / consumatore, distribuiscono i dati tra le routine in modo da aumentare attivamente la località. Inoltre, i cambi di contesto si verificano in genere solo tra unità di lavoro non al loro interno, vale a dire in un momento in cui la località è di solito minimizzata comunque.
  4. È meno probabile che il blocco delle risorse sia necessario quando le routine sanno che non possono essere arbitrariamente interrotte nel mezzo di un'operazione, consentendo alle implementazioni più semplici di funzionare correttamente.

5

Prefazione

Voglio iniziare affermando affermando un motivo per cui le coroutine non stanno ottenendo una rinascita, il parallelismo. In generale, le moderne coroutine non sono un mezzo per raggiungere un parallelismo basato sui compiti, poiché le implementazioni moderne non utilizzano la funzionalità multiprocessing. La cosa più vicina a questo sono cose come le fibre .

Uso moderno (perché sono tornati)

Le coroutine moderne sono arrivate come un modo per ottenere una valutazione pigra , qualcosa di molto utile in linguaggi funzionali come haskell, dove invece di iterare su un intero set per eseguire un'operazione, si sarebbe in grado di eseguire un'operazione solo una valutazione quanto necessario ( utile per infiniti insiemi di elementi o altrimenti insiemi di grandi dimensioni con terminazione anticipata e sottoinsiemi).

Con l'uso della parola chiave Yield per creare generatori (che di per sé soddisfano parte delle esigenze di valutazione pigre) in linguaggi come Python e C #, le coroutine, nella moderna implementazione non erano solo possibili, ma possibili senza una sintassi speciale nella lingua stessa (anche se alla fine Python ha aggiunto alcuni bit per aiutare). Co-routine aiuto con evaulation pigri con l'idea di futuro s dove se non è necessario il valore di una variabile in quel momento, è possibile ritardare l'acquisizione in realtà fino a che non esplicitamente chiedere che il valore (che consente di utilizzare il valore e valutarlo pigramente in un momento diverso dall'istanza).

Oltre alla valutazione pigra, tuttavia, specialmente nella sfera web, queste routine aiutano a risolvere l' inferno della richiamata . Le coroutine diventano utili nell'accesso al database, nelle transazioni online, nell'interfaccia utente, ecc., Dove i tempi di elaborazione sul computer client non determineranno un accesso più rapido a ciò di cui hai bisogno. Il threading potrebbe riempire la stessa cosa, ma richiede molto più overhead in questa sfera e, al contrario delle coroutine, in realtà sono utili per il parallelismo dei compiti .

In breve, man mano che lo sviluppo web cresce e i paradigmi funzionali si fondono maggiormente con i linguaggi imperativi, le coroutine sono venute come una soluzione ai problemi asincroni e alla valutazione pigra. Le coroutine arrivano negli spazi problematici in cui il threading multiprocesso e il threading in generale sono o non necessari, scomodi o impossibili.

Esempio moderno

Le coroutine in linguaggi come Javascript, Lua, C # e Python derivano tutte le loro implementazioni da singole funzioni che danno il controllo del thread principale ad altre funzioni (niente a che fare con le chiamate del sistema operativo).

In questo esempio di Python , abbiamo una divertente funzione Python con qualcosa chiamato awaital suo interno. Questo è fondamentalmente un rendimento, che porta all'esecuzione alla loopquale quindi consente l'esecuzione di una funzione diversa (in questo caso, una factorialfunzione diversa ). Si noti che quando dice "Esecuzione parallela di compiti" che è un termine improprio, non si sta effettivamente eseguendo in parallelo, la sua funzione interleaving viene eseguita attraverso l'uso della parola chiave wait (che tieni presente che è solo un tipo speciale di rendimento)

Consentono rendimenti di controllo singoli, non paralleli, per processi simultanei che non sono compiti paralleli , nel senso che questi compiti non operano mai contemporaneamente. Le coroutine non sono discussioni nelle implementazioni linguistiche moderne. Tutte queste implementazioni linguistiche delle routine di routine derivano da queste chiamate di rendimento delle funzioni (che il programmatore deve effettivamente inserire manualmente nelle routine di routine).

EDIT: C ++ Boost coroutine2 funziona allo stesso modo, e la loro spiegazione dovrebbe dare una visione migliore di ciò di cui sto parlando con i tuoi figli, vedi qui . Come puoi vedere, non c'è un "caso speciale" con le implementazioni, cose come le fibre boost sono l'eccezione alla regola e anche allora richiedono una sincronizzazione esplicita.

EDIT2: poiché qualcuno pensava che stavo parlando del sistema basato su task c #, non lo ero. Stavo parlando del sistema di Unity e delle ingenue implementazioni di c #


@ T.Sar Non ho mai affermato che C # avesse delle coroutine "naturali", né C ++ (potrebbe cambiare) né pitone (e le aveva ancora), e tutti e tre hanno implementazioni di routine. Ma tutte le implementazioni in C # di coroutine (come quelle in unità) sono basate sulla resa come descrivo. Anche il tuo uso di "hack" qui non ha senso, immagino che ogni programma sia un hack perché non è sempre stato definito nella lingua. Non sto mescolando in alcun modo il "sistema basato su attività" C # con nulla, non ne ho nemmeno parlato.
quando

Suggerirei di rendere la tua risposta un po 'più chiara. C # ha sia il concetto di istruzioni di attesa che un sistema di parallelismo basato su attività - l'utilizzo di C # e di quelle parole mentre fornisce esempi su Python su come Python non sia realmente parallelo può causare molta confusione. Inoltre, rimuovi la tua prima frase: non è necessario per attaccare direttamente altri utenti in una risposta del genere.
T. Sar - Ripristina Monica il
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.