Pagina didattica di G. Servizi
Home >> Linguaggio C++


livello altissimo

Canto trentanovesimo: i threads

per tornare indietro...



per raggiungere immediatamente un canto

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
36 37 38 39

Così di ponte in ponte, altro parlando
che la mia comedìa cantar non cura,
venimmo; e tenavamo 'l colmo, quando

restammo per veder l'altra fessura
di Malebolge e li altri pianti vani;
e vidila mirabilmente oscura.

Quale ne l'arzanà de' Viniziani
bolle l'inverno la tenace pece
a rimpalmare i legni lor non sani,

ché navicar non ponno - in quella vece
chi fa suo legno novo e chi ristoppa
le coste a quel che più vïaggi fece;

chi ribatte da proda e chi da poppa;
altri fa remi e altri volge sarte;
chi terzeruolo e artimon rintoppa - :

tal, non per foco ma per divin'arte,
bollia là giuso una pegola spessa,
che 'nviscava la ripa d'ogne parte.

E quale esordio più alto poteva avere questo canto, con cui il percorso di apprendimento del C++ si conclude? Anche la nostra comedìa non ha curato di cantare molte cose prima di giungere a questo passo, in cui ci apparirà mirabilmente oscura la misteriosa congerie dei cosiddetti threads di esecuzione o, semplicemente, threads.
Anche l'immagine della frenetica attività invernale degli arsenali della Serenissima Repubblica di San Marco calza a pennello e perfino la tenace pece, al cui solo suono par di averla appiccicata addosso, e la pegola spessa fanno la loro figura e si adattano a quello di cui ci si accinge a discutere.

In effetti, con l'introduzione nel linguaggio dei threads, avvenuta nel 2011, si apre a un programma la possibilità di eseguire alcune sue parti CONTEMPORANEAMENTE, sfruttando a fondo le architetture multi-processore dei calcolatori nostri contemporanei (bisticcio VOLUTO).
Pertanto, se tornate con la mente al canto in cui si parlava di sequenzialità e pietre miliari, ORA capite che quei ragionamenti andrebbero completati aggiungendovi la frase "tutto ciò a meno che non si introducano dei threads, perché, in tal caso, quanto è stato scritto si restringe al singolo thread ma non concerne un thread nei confronti di un altro".

Quando, in un programma, si creano dei threads è come se l'esecuzione del programma stesso, che fino a quel momento procedeva spedita lungo un unico binario, incontrasse un ventaglio di scambi e quindi il binario si ramificasse in numerosi binari paralleli, tanti quanti sono i threads, tutti occupabili da singole parti diverse del codice. Allora veramente chi ribatte da proda e chi da poppa; veramente altri fa remi e altri volge sarte; veramente la carrozza di coda del convoglio del nostro programma può affiancare quella di testa e andare avanti ciascuna per suo conto per un periodo di tempo stabilito dal programmatore, ossia fino a quando tutti i binari originati al ventaglio di scambi si riuniscono novamente in un solo binario, con la ricomposizione corretta dell'intero convoglio.

Se aveste prestato la dovuta attenzione, dovreste aver notato che è stato usato il verbo creare a proposito dei threads: in effetti essi non sono altro che OGGETTI (Μεγαλη Γλωσσα [scusate, ma stavolta andava detto in greco...]) e come tali possono essere creati in ogni momento dal buon programmatore come istanze di una certa classe, per l'appunto la classe std :: thread, definita, come si vede, nel namespace std e fruibile con l'inclusione # include <thread>.

Per rendersi conto della portata di quello di cui si sta tentando di parlare è sufficiente considerare il seguente miserrimo programma, che costituisce per voi una sorta di viaggio nella memoria del tempo in cui eravate ancora piccoli (intendo piccoli rispetto all'apprendimento del linguaggio):

#include <iostream>
#include <thread>

namespace thread_space
{
int s;
}

using namespace std;
using namespace thread_space;

void f1(int n)
{
s = 11;
for (int i = 0; i < 5; ++i) {
cout
<< "Io sono la funzione f1 con s = "
<< s << " e n = " << n
<< " \n" << flush;
}
}

void f2(int& n)
{
s = 22;
for (int i = 0; i < 5; ++i) {
cout
<< "Io sono la funzione f2 con s = "
<< s << " e n = " << n
<< " \n" <<flush;
++n;
}
}

int main(  )
{
int n = 0;
f1(n+1);
f2(n);
cout
<< "Alla fine n vale " << n
<< " e s vale " << s << '\n';
}


L'esecuzione del programma (l'avete eseguito?) non presenta la minima sorpresa: f1 è invocata per prima e si esegue senza che di f2 si abbia alcun sentore; quando questa sopraggiunge f1 ha finito da un pezzo, e di lei non si sente più parlare. Gli effetti sulle variabili n, dichiarata in main, e s, dichiarata in un namespace del tutto equivalente a std, sono assolutamente scontati; l'inclusione # include <thread> non serve a niente.

Ora osservate (ED ESEGUITE!) la seguente variante, compilandola con l'aggiunta dell'opzione -pthread al comando di compilazione che usate abitualmente:

#include <iostream>
#include <thread>

namespace thread_space
{
thread_local int s;
}
using namespace std;
using namespace thread_space;
void f1(int n)
{
s = 11;
for (int i = 0; i < 5; ++i) {
cout
<< "Io sono la funzione f1 con s = "
<< s << " e n = " << n
<< '\n' << flush;
this_thread::sleep_for(chrono::milliseconds(0));
}
}
void f2(int& n)
{
s = 22;
for (int i = 0; i < 5; ++i) {
cout
<< "Io sono la funzione f2 con s = "
<< s << " e n = " << n
<< '\n' <<flush;
++n;
this_thread::sleep_for(chrono::milliseconds(0));
}
}
int main(  )
{
int n = 0;
thread t1(f1, n + 1);
thread t2(f2, ref(n));
t1.join(  );
t2.join(  );
cout
<< "Alla fine n vale " << n
<< " e s vale " << s << '\n';
}


Alle due funzioni è stata solo aggiunta l'invocazione di una funzione che produce una sospensione fasulla dell'esecuzione della durata di 0 (zero!) millisecondi; serve a ingannare il compilatore, e indurlo a lasciarle eseguire ciascuna nel SUO thread, in modo da poter apprezzarne l'effetto: senza quella chiamata non si sarebbe visto proprio niente perché le due funzioni sono talmente semplici che il compilatore avrebbe pensato qualcosa come "non scherziamo; qui mi si prende in giro"... e così l'abbiamo preso in giro sul serio...

Venendo alle parti significative del programma, le uniche variazioni SERIE si hanno in main e nel namespace thread_space in cui la variabile s viene qualificata con la parola di vocabolario thread_local.
In main, al posto della semplice invocazione sequenziale delle due funzioni f1 e f2, c'è appunto la creazione di due oggetti thread, ai cui costruttori sono trasmessi, nell'ordine, i nomi delle funzioni e i loro argomenti: si apprezzi l'uso della funzione ref per trasmettere al costruttore di t2 il secondo argomento in modo che, grazie al perfect forwarding, f2 possa riceverlo come lo richiede.

Dovreste capire immediatamente che il costruttore parametrico di thread non può che essere una funzione templatizzata variadica (e come farebbe, se no?) e che il suo PRIMO argomento deve essere una classe templatizzata capace di essere istanziata sia con funzioni sia con classi a loro volta templatizzate (è o no un gran linguaggio?).

Anche nella nostra semplicissima realizzazione si vede distintamente che le due funzioni sono eseguite simultaneamente e che alla fine, stavolta, main si trova la variabile s intonsa, dato che l'effetto della qualifica thread_local consiste nel produrre una variabile distinta, chiamata sempre s, in OGNI thread che ne faccia uso: e main NON HA MAI reinizializzato la s del SUO thread.

Le esecuzioni dei metodi membri join hanno lo scopo di reimmettere il binario parallelo sul binario principale: sono la ricomposizione del convoglio; solo quando OGNI thread ha eseguito il suo join il programma tornerà a proseguire normalmente e anche si dirigerà normalmente verso la sua naturale chiusura.

Ora immaginate per UN SOLO ISTANTE che di threads diversi ce ne sia un intero contenitore, di quelli del canto scorso, pieno da traboccare, e che ciascuno svolga operazioni non così semplici come quelle delle nostre funzioni e magari anche in grado di influenzare quelle degli altri threads; immaginate anche di essere, come SIETE, il capostazione che dirige il traffico su questa complicata rete ferroviaria costituita dal vostro programma: di che cosa avreste bisogno?

La risposta non è difficile: di posti di blocco, semafori e ferree regole di sincronizzazione e/o precedenza.

Fortunatamente tutto ciò è disponibile (c'era dubbio?) nella classe thread attraverso i suoi metodi membri e le regole generali del linguaggio.

Nel seguente esempio si fa cenno fugace a tutto ciò, sempre a un livello superficiale, ma non del tutto basso; e si mostra anche uno schema di interfacciamento dei threads con istanze di classi e con contenitori della standard template library come vector. Il codice, di per sé, non fa nulla di particolarmente interessante, ma mostra alcune soluzioni efficaci e anche l'uso di un mutex, ossia di un semaforo rosso che fa aspettare TUTTI gli altri threads fino a quando quello che l'ha acceso non lo spegne.

#include <iostream>
#include <sstream>
#include <vector>
#include <iterator>
#include <thread>
#include <mutex>

using namespace std;

struct Ciccio
{
// qualsiasi congerie di metodi e variabili
static unsigned int indice_primario;
unsigned int indice_corrente;
Ciccio(  ) {indice_corrente = ++indice_primario - 1;}
};

unsigned int Ciccio::indice_primario = 0;

mutex mutex_;

template <class X> struct Catena : vector<X>
{static thread_local Catena* catena_stazionaria;
static Catena* catena_stazionaria_di_prima;
using vector<X>::size;
using vector<X>::begin;
using ITERATOR = typename vector<X>::iterator;
ITERATOR it;
void algo(Catena*& c, unsigned int i)
{
ostringstream os;
catena_stazionaria = new Catena;
*catena_stazionaria = *c;
mutex_.lock(  );
os << "Sto eseguendo il thread " << i
<< " che usa l'elemento # "
<< (*(catena_stazionaria->it+i)).indice_corrente
<< '\n' << flush;
mutex_.unlock(  );
clog << os.str(  ).data(  );
delete catena_stazionaria;
}
Catena(  )
{
it = begin(  );
}
void esegui(  )
{catena_stazionaria_di_prima = new Catena;
*catena_stazionaria_di_prima = *this;
size_t s = size(  );
void (Catena::*f)(Catena*&, unsigned int) = &Catena::algo;
thread * threads = new thread[s];
clog << "avviamento dei threads\n" << flush;
for(int i=0; i != s; ++i)
threads[i] = thread(f, this, ref(catena_stazionaria_di_prima), i);
for(int i=0; i != s; ++i) threads[i] . join(  );
clog << "threads eseguiti e riallineati con successo" << endl;
delete catena_stazionaria_di_prima;
}
};

template <typename X> thread_local Catena<X>*
Catena<X>::catena_stazionaria;
template <typename X> Catena<X>* Catena<X>::catena_stazionaria_di_prima;

int main(  )
{
Ciccio ciccio[200];
clog
<< "creati " << Ciccio::indice_primario
<< " oggetti Ciccio" << endl;
Catena<Ciccio> catena;
for(auto i : ciccio) catena.push_back(move(i));
clog
<< "catena detiene " << catena.size(  )
<< " oggetti Ciccio" << endl;
catena.it = catena.begin(  );
catena . esegui(  );
}


Dal programma appena scritto si impara quanto segue:

  1. a ereditare un proprio contenitore da uno di quelli della standard template library; è il caso della template struct Catena, erede di vector, che consente la creazione di una Catena<Ciccio>.

  2. il modo di riempire il contenitore, utilizzando il metodo push_back ereditato, cui si trasmettono riferimenti destri agli elementi dell'array di Ciccio: ciò evita la generazione di copie di tali oggetti dato che ne viene eseguito il move constructor; d'altronde l'array dichiarato ciccio non serve a main se non come contenuto di Catena, che si incarica di gestirlo a dovere.

  3. l'aggiornamento degli iteratori di Catena tramite la linea di codice
    catena.it = catena.begin(  );
    con cui ci si cautela da eventuali perdite di validità degli iteratori occorse in fase di riempimento del contenitore.

  4. l'aver dovuto portare in ambito, durante la definizione di Catena, i metodi ereditati esplicitamente citati (begin e size) tramite le direttive using: Catena è una template e senza tali direttive il name lookup di begin e size sarebbe fallito (ANDATEVI A RILEGGERE il documento sul name lookup).

  5. l'ulteriore apparizione della parola using come facente funzioni di typedef nella dichiarazione del tipo Catena::ITERATOR.

  6. il fatto che la struct Ciccio mantenga traccia del numero delle proprie istanze in una variabile membro static (indice_primario) e che OGNI istanza abbia una variabile membro (indice_corrente) il cui valore ne rappresenta la collocazione ordinale nel contenitore Catena.

  7. il fatto che il contenitore Catena abbia due membri static che sono puntatori alla stessa template struct (catena_stazionaria e catena_stazionaria_di_prima), il primo dei quali ha l'ulteriore qualifica thread_local.

  8. l'attribuzione del valore di *this al puntatore catena_stazionaria_di_prima da parte del metodo membro esegui; si tratta sostanzialmente di una fotografia istantanea scattata al contenitore nel momento in cui il metodo inizia a eseguirsi.

  9. la dichiarazione, da parte di esegui, del puntatore a membro f, inizializzato coerentemente, rispetto alla segnatura del puntatore, con l'offset del metodo membro algo (accorciativo di algoritmo). Ed è in sostanza proprio la volontà o la necessità di aggiungere questo NOSTRO algoritmo che ci ha suggerito di rendere la struct Catena erede di vector.

  10. l'allocazione, da parte di esegui, di un puntatore alla classe thread capiente di un numero di oggetti pari al numero di Ciccio contenuti in *this; dato che questi oggetti sono stati costruiti dal costruttore di default della classe thread NON SERVONO (ANCORA) A NIENTE, ma non di meno ESISTONO.

  11. l'assegnamento, a CIASCUN oggetto puntato dal puntatore threads, del valore di un oggetto INNOMINATO della sua stessa classe costruito dal costruttore parametrico con l'indicata segnatura: È QUESTO IL MOMENTO in cui i diversi thread si avviano SIMULTANEAMENTE, ciascuno sul proprio binario.
    Deve essere ASSOLUTAMENTE tenuto presente che la classe thread NON È COPIABILE ed è per questa ragione che ne è stato richiesto il move assignment operator (ricordate? RICORDATE!). Tanto per essere ancora più chiari, una linea che recitasse
    threads[1] = threads[0];
    sarebbe bollata dal compilatore, non come errore, ma come atrocità (per inciso: CAPITE ADESSO il perché della move semantic?).

  12. la segnatura del costruttore variadico di thread utilizzato, da confrontarsi con quella del precedente esempio: colà il primo parametro trasmesso era sempre stato il nome di una funzione definita nell'ambito globale; qui invece si tratta di un puntatore a un metodo membro di una classe. Tanto basta al costruttore, che sa riconoscere la differenza, per capire il significato del secondo argomento ricevuto, che dovrebbe essere ancora più chiaro a chi legge, e per concludere che nel thread che è chiamato a costruire deve essere eseguita la funzione

    this   ->*  f(catena_stazionaria_di_prima, i);
    ossia
    this   ->  algo(catena_stazionaria_di_prima, i);

    proprio come se tale linea fosse stata esplicitamente scritta dal programmatore, a parte l'invocazione della funzione ref, necessaria per il perfect forwarding (ricordate? RICORDATE!), dato che il metodo puntato da f, ossia Catena::algo, riceve il suo primo argomento per riferimento.

  13. il fatto che, subito dopo l'avvio dei threads, essi siano tutti ricomposti tramite l'invocazione del metodo join appartenente a ciascuno.

  14. il fatto che, NEL FRATTEMPO, sono state eseguite, in maniera del tutto ASINCRONA, size(  ) diverse versioni del metodo algo, TUTTE COMUNQUE APPARTENENTI ALL'OGGETTO *this, il che significa che tutte avevano a propria disposizione lo STESSO INTERO CORREDO di variabili e metodi dell'oggetto...

  15. ...eccettuati i membri qualificati thread_local: di questi OGNI thread ha la propria PERSONALE ISTANZA, del tutto diversa e distinta dall'omologa istanza di quel membro detenuta da OGNI ALTRO thread. Significa che ciascuno di essi gestisce una versione distinta e separata del puntatore catena_stazionaria. Queste distinte versioni si estinguono automaticamente con l'estinzione del thread di pertinenza.

  16. il modo di lavorare tenuto dal metodo algo; non che sia l'unico possibile: ricordate l'assioma secondo cui il C++ non fornisce mai una soluzione UNICA.



Ci si soffermi appunto su Catena::algo, tenendo ben presente che ne esistono numerose versioni in esecuzione contemporanea, ognuna con un diverso valore ricevuto per l'argomento unsigned int i, rispecchiante quello della variabile indice_corrente dell'elemento i-esimo di *this.
In questo modo algo, pur avendo a disposizione TUTTO il contenuto di *this, è informata su quale degli elementi che *this contiene debba rivestire per sé un significato, diciamo così, speciale.

Non solo: algo prende anch'essa una fotografia istantanea di *this prima di cominciare a lavorare, e la memorizza nella PROPRIA COPIA del puntatore membro thread_local catena_stazionaria. In questo modo non solo potrà essere in grado di confrontare il valore corrente di *this, che magari lei stessa modifica, col suo valore iniziale, ma anche con eventuali modifiche apportate da (lei stessa in) altri threads (GRAN LINGUAGGIO).

Tali modifiche potrebbero anche generare delle cosiddette data races, ossia situazioni conflittuali in cui (almeno) due threads pretenderebbero di accedere/modificare contemporaneamente lo stesso dato: il programma, nella versione presentata, non fa niente di tutto questo semplicemente perché non fa niente ai dati, ma, nella vita reale, perché non potrebbe accadere?

Ecco perché viene in soccorso l'oggetto mutex_ della classe mutex, che è una crasi di mutual exclusion: in questo programma è stato dichiarato nell'ambito globale per amor di brevità, ma evidentemente nulla vieterebbe di farlo diventare membro di appropriate classi.

Quando un certo thread ne esegue il metodo membro lock quel thread continua la propria esecuzione sospendendo quella di OGNI ALTRO thread concorrente. È il semaforo rosso per tutti gli altri binari. Il verde scatta SOLO quando lo stesso thread esegue il metodo unlock: è evidentemente indispensabile NON DIMENTICARSI DI ESEGUIRLO, altrimenti il programma si mette da solo in una situazione peggiore di quella di Falstaff e Ford quando dovevano uscire dalla porta dell'osteria della giarrettiera.

Anche nella versione presentata c'è un minimo rischio di incorrere in quella situazione, ossia semaforo rosso su TUTTI I BINARI, qualora TUTTI i threads eseguissero mutex_lock(  ); nello STESSO, IDENTICO, clock di macchina: una situazione, invero, con probabilità ridicola di accadimento; niente paura; esistono anche dei mutex temporizzati che diventano verdi COMUNQUE allo scadere di un tempo prefissato dal programmatore; e in quel caso diventa essenziale poter fare dei confronti sui dati sensibili per evitare di riavviare il convoglio del programma non si sa come. Ed ecco a che cosa può servire aver preso delle fotografie...

Dato che, tra l'esecuzione di mutex_.lock(  ); e quella di mutex_.unlock(  );, il thread che le esegue assume l'esclusiva della gestione dell'intera memoria del programma, sarà compito del buon programmatore far eseguire quelle operazioni che avessero la potenzialità di generare data races, realizzando in tal modo il compromesso MIGLIORE che contempli SIA la loro esclusione SIA la velocizzazione spudorata dell'esecuzione del codice.

Nel nostro esempio l'effetto apportato dall'utilizzo del mutex è immediatamente apprezzabile semplicemente togliendolo: rieseguite dunque il programma dopo aver commentato le richieste di esecuzione SIA di mutex_.lock(  ); SIA di mutex_.unlock(  ); e aver sostituito os << (s'intende in algo) con clog <<, e poi venite a riferirmi, se volete, che cosa vi è successo.

Un'ultima annotazione sul modo di gestire l'output da parte di algo; premesso che, in generale, una funzione threadizzata, consentitemi il neologismo,

MENO OUTPUT FA MEGLIO È, e al limite farebbe molto bene a starsene ZITTA

(prendetelo come un assioma) e assodato che COMUNQUE è sempre meglio fare output provvisori in memoria, riservandosi l'uscita sui canali standard solo quando l'output stesso è terminato, dovrebbe risultare chiaro, alla luce anche di quanto avverrebbe usando clog senza mutex (l'avete fatto l'esperimento?), il motivo per cui si è scelto di adottare la soluzione che comporta l'uso di un ostringstream, visto che la funzione, dovendo servire da esempio, bisogna pure che faccia qualcosa. In effetti, mentre clog è visibile in ugual misura a TUTTI i threads, l'oggetto os della classe ostringstream pertiene in esclusiva ad algo, essendo dichiarato nel SUO ambito e quindi è DIVERSO tra un thread e l'altro.

A volte, secondo il tipo di operazioni compiute nei diversi thread e secondo la complessità dell'influenza reciproca che possono avere, l'uso di semplici semafori, fossero o no temporizzati, potrebbe rivelarsi insufficiente. Pensate alla linea ferroviaria CastelBolognese-Ravenna, che ha un solo binario: i treni da Ravenna sono "eseguiti" in un thread e quelli per Ravenna in un altro, ed è evidente a tutti che questi due thread si influenzano reciprocamente in maniera pesante, come sperimentano quotidianamente, sulla propria pelle, i passeggeri di quella linea. In genere l'orario ferroviario prevede appunto dei mutex in caso di data race di due treni, ciascuno di un thread, sulla stazione di Lugo, nel senso che si prevede che entrambi la impegnino simultaneamente, di modo che possano ripartire in santa pace, ognuno per il suo thread, senza alcun'altra interruzione se non quella, del tutto fisiologica, che consenta la salita/discesa dei viaggiatori in arrivo o in partenza. Nella malaugurata, e purtroppo QUOTIDIANA, ipotesi che uno dei due treni, o verosimilmente entrambi, sia in ritardo, quello che, comunque, arriva per primo subisce un mutex (semaforo rosso) che lo lascia fermo nella stazione fino a quando non vi sopraggiunga il ritardatario. Questa situazione è, tutto sommato, accettabile nella quasi totalità dei giorni grami vissuti da ogni pendolare...finché si tratti di ritardi di pochi minuti...Ma che succederebbe se il treno per Ravenna investisse una vacca che pascolava sui binari tra Castel Bolognese e Solarolo, e occorresse attendere che il veterinario legale effettui l'autopsia sulla bovina per stabilirne con certezza le cause del decesso? Pur con tutta la solidarietà che si dovrebbe alla sventurata quadrupede, i passeggeri del treno sottoposto a mutex a Lugo si spazientirebbero, soprattutto se non potessero prevedere in alcun modo la durata dello stallo. Allora, di solito, accade che l'altoparlante della stazione di Lugo emetta un comunicato in cui, forse, "s'informano i signori viaggiatori comequalmente la mucca Carolina è deceduta lungo la linea ferroviaria..." e poi segue un pronostico temporale su quanto a lungo si protrarrà la sosta aggiungendo che, per chi volesse o potesse, si allestirà un autobus sostitutivo sul piazzale esterno della stazione che partirà comunque non prima che sia trascorsa una volta e mezza il tempo pronosticato (la qual cosa fa capire che è meglio prendere l'autobus).

Se uno decifra questa "macabra favoletta" tentando di trarne una morale, non potrà non accorgersi che l'annuncio dell'altoparlante è la promessa dell'avverarsi di un evento che, però, accadrà in un futuro non precisamente definito. Quando (e se) accadrà la linea potrà sbloccarsi e riprendere il suo funzionamento ordinario, pur con tutti i ritardi del mondo; ma, nel frattempo, si propone un'alternativa...

È esattamente quel che fanno le due classi template chiamate rispettivamente promise e future (ma tu guarda che nomi... GRAN LINGUAGGIO) e che lavorano in sinergia fortissima e proficuissima con i thread. Restando nell'apologo ferroviario, è come se il treno fermo a Solarolo (leggi: un thread che sta svolgendo un lavoro lungo) creasse un oggetto della classe promise in cui pone appunto, inizializzandone opportuni membri, la "promessa" che il suo lavoro avrà un risultato; questi oggetti di tipo promise sono visibili dagli altri thread (il treno di Lugo) tramite oggetti di tipo future, che, al momento giusto, conterranno il risultato lungamente atteso e daranno, solo da quel momento, via libera alla prosecuzione del thread in attesa. Tra la creazione dell'oggetto promise e l'inizializzazione dell'oggetto future, il thread in attesa e, a maggior ragione, tutti gli altri eventuali thread cui di quell'oggetto promise non importasse un fico secco, può/possono continuare a svolgere ogni altra operazione che fosse indipendente dal risultato atteso, magari creando APPOSTA un ulteriore thread dedicato. Per i dettagli fini riguardanti i metodi e le variabili membro di queste due classi occorrerebbe scrivere almeno un altro canto...

Ma è tempo ormai di ormeggiare la piccioletta barca e bersi un ottimo punch al rhum.

Se voleste salpare di nuovo per andare a esplorare il continente dei threads fin nella giungla più fitta e fino a civilizzare i cannibali che vi potreste incontrare ... e magari andare a scoprire le terre nuove che si trovano al di là degli oceani degli editandi standard 2014 e 2017 ... non avete che da richiamare il nostromo...finché dura...

A l'alta fantasia qui mancò possa;
ma già volgeva il mio disio e 'l velle,
sì come rota ch'igualmente è mossa,

l'Amor che move il sole e l'altre stelle.

per tornare indietro...

per raggiungere immediatamente un canto

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
36 37 38 39
Torna in cima alla pagina