m15. programmazione concorrente in c++11, parte 2

Transcript

m15. programmazione concorrente in c++11, parte 2
M15. PROGRAMMAZIONE CONCORRENTE IN C++11, PARTE 2
e future vanno bene per quelle situazioni in cui le mie parti dell’algoritmo sono
debolmente interconnesse. In altre situazioni, invece, siamo interessati a gestire esplicitamente alcuni aspetti.
async
La classe std::thread
La classe std::thread modella un thread del sistema operativo, restituendo un’interfaccia indipendente da quest’ultimo. Per poter creare un thread dobbiamo passare al suo
costruttore un oggetto chiamabile (funzione o oggetto funzionale passato per valore).
Potenzialmente il costruttore può accettare più argomenti. Creando un oggetto std::thread viene costruito un nuovo thread all’interno del sistema operativo, la cui esecuzione
è immediata.
#include <thread>
void f() {
std::cout << "Up & Running!" << std::endl;
}
int main() {
std::thread t(f);
/* Altre operazioni nel thread principale */
t.join();
}
/* Inizio del thread t */
/* Si blocca e aspetta che t termini */
Nell’esempio creo un thread passandogli la funzione di cui voglio l’esecuzione. Per sapere se il thread ha finito utilizziamo t.join(), rimanendo bloccati fin quando il thread non
ha terminato la sua esecuzione.
Come già detto, eventuali oggetti che implementano operator() vengono passati per
valore, quindi l’oggetto originale resta immutato. Spesso e volentieri usiamo quindi funzioni lambda, catturando per reference in modo che il thread, durante la sua esecuzione,
possa modificare tali valori. Fare le cose in questo modo deve metterci sull’attenti, perché è facile che i riferimenti passati muoiano nel mentre che il thread è eseguito: devo
dunque accoppiare il ciclo di vita delle variabili passate al thread al ciclo di vita del thread stesso.
Differenze con async/future
Rispetto al precedente sistema di esecuzione concorrente con async e future ci sono alcune differenze. Non si può infatti scegliere la politica di attivazione, quindi se non ci
sono le risorse si lancia un std::system_error.
Con async, inoltre, avevamo un future: in questo modo sapevamo dov’era il risultato e
questo era protetto. Con i thread non c’è un meccanismo standard per accedere al risultato e dobbiamo fare tutto noi: l’unica cosa che ci viene “regalata” è un id.
Se all’interno della funzione chiamata in un thread nasce un’eccezione e nessuno ne fa
il catch, lo stack si contrae. Si esce dalla funzione e si entra nella C runtime library in
quanto parte prima una funzione standard che avvia la nostra funzione dentro un blocco
try/catch: siccome nella catch c’è l’exit, il programma viene abortito.
Ciclo di vita di un thread
Analogamente al caso dei future in cui è necessario prima o poi chiamare il metodo
get(), con i thread bisogna invocare una join() o, alternativamente, una detach() (in
cui lasciamo il thread libero per la sua strada, perdendone tutti i contatti).
Non fare nessuna di queste due cose prima di distruggere il thread implica che il programma si pianti, in quanto il thread ha delle risorse e non vuole creare leakage.
Queste considerazioni implicano che la creazione di N thread – anche detached – con
successivo ritorno del main che uccide thread ancora pendenti (in quanto è eseguita
una ExitProcess) può portare a porcate solenni, come file mezzi scritti e mezzi no. E’
compito del programmatore evitare situazioni di questo genere.
Restituire un risultato: std::promise<T>
Una volta che il thread ha fatto il suo mestiere vorrei sapere il risultato della computazione. Per far questo deve necessariamente esistere una variabile condivisa, che con il
meccanismo di async/future è “nascosta” mentre in questo caso è da gestire esplicitamente.
Fare questo ci dà un sacco di responsabilità al riguardo: come facciamo a sapere quando
la variabile condivisa contiene qualcosa di sensato? Una prima idea è vedere la morte
del thread. Buona, ma non buonissima: in primo luogo il thread potrebbe morire prima,
inoltre ci potrebbero essere thread che restituiscono risultati man mano. Pensare a una
variabile booleana che ci dice quando il dato è pronto è una fesseria clamorosa (come
faccio a sapere quando il valore contenuto nella variabile è sensato?)
C++ mette una pezza a questo problema comune mettendo a disposizione un oggetto
di tipo std::promise<T>, che modella un dato condiviso ligio a restituire, prima o poi, un
oggetto di tipo T. Gli oggetti promise inglobano un future legato alla particolare promessa.
#include <future>
void f(std::promise<std::string>& p) {
try {
/* Calcolo il valore da restituire */
std::string result = ...;
p.set_value(std::move(result));
} catch (...) {
p.set_exception(std::current_exception());
}
}
int main() {
std::promise<std::string> p;
/* Creo un thread e forzo p ad essere passata per riferimento */
std::thread t(f,std::ref(p));
t.detach();
/* ... */
std::string res = p.get_future().get();
/* Accedo al risultato */
}
In questo esempio abbiamo un thread che a fine elaborazione deve restituire una stringa. Quando lo creiamo gli passiamo una std::promise<std::string> per reference (perché ci deve scrivere dentro). Se c’è un problema che lancia una qualunque eccezione,
questa viene salvata nell’oggetto promise e l’uscita sarà fatta in modo pulito.
Prima di creare il thread dunque creo una promise condivisa tra il thread principale
(main) e il thread secondario. Creo il thread che deve eseguire f e gli passo un reference a p.
Una volta creato il thread me lo posso dimenticare ( detach) senza dovermi mettere in
attesa, perché quando voglio accedere al risultato uso la future associata alla promise.
Passando più promise in ingresso posso far sì che il thread produca più risultati.
E’ importante che si sia eseguita l’istruzione detach(), in modo da non essere legato dal
meccanismo della join(). Così facendo, però, non possiamo più sapere quando il thread
è terminato (perlomeno in maniera diretta) e la cosa può essere fonte di problemi: supponiamo, ad esempio, di avere un thread detacchato che debba scrivere nella variabile
globale pippo. Quando il main() termina non si esce immediatamente, ma vengono prima distrutte tutte le variabili globali sotto al naso del thread ancora in esecuzione.
Thread distaccati
Se il thread principale termina (sia bene che a causa di ExitThread o ExitProcess) l’intero processo viene terminato, con tutti i thread distaccati eventualmente presenti.
Come posso evitare che il thread principale finisca prima che gli altri abbiano finito? C’è
quick_exit che ci permette di terminare un programma senza invocare distruttori di variabili globali, ma è una roba da maneggiare con cura (perché magari sto linkando librerie che fanno assunzioni sul codice)
Promise e corse critiche
promise permette di inserire una forma di sincronizzazione, in quanto c’è la possibilità di
sapere se questa è stata soddisfatta o meno. Insieme alla promise c’è un metodo get()
che mi permette di sapere quando il dato è buono. Ottenere un dato non vuol dire però
che il thread sia finito, in quanto quest’ultimo potrebbe fare qualche altra operazione
prima di uscire.
Per questo motivo, promise – oltre alla set_value – offre anche il metodo per impostare il
valore all’uscita del thread, ovvero set_value_at_thread_exit. Questo garantisce a chi
sta chiamando la get() sulla promise sia che il dato è buono e che il thread corrispondente è finito. C’è un analogo anche con le eccezioni. Ovviamente introduco un po’ di
overhead.
Conoscere la propria identità
Un thread viene creato dal sistema operativo e riceve un identificatore univoco (più significativo in Linux – dove un thread esiste perché rappresentato dal numero N – che in
Windows). std::this_thread è una classe che modella il thread corrente e permette di
interagire con esso: con get_id() ci viene restituito l’ID del thread corrente.
Sospendere l’esecuzione
std::this_thread offre anche le seguenti funzioni statiche
• sleep_for: sospendo l’esecuzione del thread per un certo tempo
• sleep_until: sospendo fino a un certo momento
inoltre serve a “passare la mano” dicendo allo scheduler che io in teoria avrei
anche da fare, ma se c’è qualcuno che ha da fare può lavorare. E’ solo un suggerimento
allo scheduler, che può non sfruttarlo. In C# yield() ha un funzionamento più significativo.
yield()
***