«

»

ott 11

Il C++ e il Multithreading (C++0X – C++11)

Un’altra funzionalità molto interessante introdotta nel C++0X oltre le lambda descritte nell’articolo precedente è il supporto nativo al multithreading.

Esistevano già librerie che si occupavano di multithreading in cpp ma adesso è integrato nel linguaggio e nella sua libreria standard anche se il backend che effettivamente alloca i thread è l’onnipresente libreria pthread.
Cominciamo allora :) do per scontato che abbiate un minimo di familiarità con la programmazione concorrente e come al solito i particolari per la compilazione ( che stavolta potrebbe essere più complicata del solito) sono alla fine dell’articolo

Allochiamo il nostro primo thread

Un thread è un qualcosa che viene eseguito in parallelo, e quindi si crea attraverso una funzione che è quello che deve fare, non è necessario creare una classe intorno al thread come si farebbe in java, basta passare al costruttore di thread la funzione che deve eseguire il thread costruito o qualcosa di equivalente

#include <iostream>
#include <thread>
#include <cstdlib>

using namespace std;

void cosadafare(){
 cout<<"Sono una procedura!"<<endl;
}

class Dummy
{
public :
 void operator()()
 {
 for(int a=0;a<5;a++){
 cout<<"Sono un oggetto, e quindi più figo di una procedura! "<< a <<endl;
 usleep( 1000 * ( rand() % 900 + 100 ));
 }
 }
};

void uccidimi(){
 for(int a=0;a<50;a++){
 cout<<"Io parlo troppo "<< a <<endl;
 usleep( 1000 * ( rand() % 900 + 100 ));
 }
}

int main()
{
 Dummy w;
 std::thread t1(w);
 std::thread t2(cosadafare);
 std::thread t3(uccidimi);

 t1.join();
 t2.join();
 t3.detach();
};

Cpp è un linguaggio marcatamente OOP e infatti non è necessario passargli una procedura, basta qualunque ogetto supporti l’operatore (), possiamo anche creare una classe che definisce questo operatore e passarne un istanza.

E’ interessante notare che quando si chiama un thread su un oggetto, come nel caso di std::thread t1(w), non gli viene passato il vero oggetto ma una sua copia, ma di questo ne parliamo dopo.

Le join servono ad aspettare che i thread finiscono prima di terminare il programma, infatti i thread sono concettualmente ‘figli’ del processo padre ( il fatto che lo siano veramente dipende dall’implementazione) e se terminasse il processo padre i figli verrebbero terminati di conseguenza facendo generare a linux un allarmante messaggio di warning, per terminare un thread prima che abbia finito il suo lavoro si usa detach

Thread e condivisione dei dati

Uno dei vantaggi dei thread rispetto ai processi è che condividono lo spazio di memoria tra di loro, tutto quello che è nello spazio di indirizzamento del processo padre può essere passato ai figli.

Quindi risulta molto conveniente piazzare dentro un oggetto la procedura del thread e i dati in cui dovrà lavorare ma c’è un inconveniente: come dicevamo sopra
quando si chiama un thread su un oggetto, come nel caso di std::thread t1(w), non gli viene passato il vero oggetto ma una sua copia , questo è irrilevante nel caso di oggetti che wrappano solo una procedura ma può essere una fonte di bug subdoli nel caso essi contengano dati infatti modificando l’oggetto non modifichiamo niente all’interno del thread perché sono oggetti diversi

se vogliamo evitare questo comportamento dobbiamo forzare un passaggio via reference e non abbiamo modo di farlo col linguaggio, dobbiamo affidarci alla magia nera delle stl,la soluzione è: std::thread t1(std::ref(dw));

Vediamo un esempio

#include <iostream>
#include <thread>
#include <cstdlib>
#include <string>

using namespace std;

class Dummy
{
public :

 string testo;
 string nome;
 void operator()()
 {
 for(int a=0;a<15;a++){
 cout<<nome << " dice: " << testo<< " " << a <<endl;
 usleep( 1000 * ( rand() % 900 + 100 ));
 }
 }
};

int main()
{
 Dummy w,w2;
 w.testo="ammaccabanane";
 w.nome="thread1";
 w2=w;
 w2.nome="thread2";
 std::thread t1(w);
 std::thread t2(std::ref(w2));
 sleep(2);
 w.testo="Yikes!";
 w2.testo="Yikes!";
 t1.join();
 t2.join();
};

Eseguendolo noterete che il comportamento del primo thread resta invariato, mentre possiamo agire sul secondo modificando i suoi dati dall’esterno.

E se volessi passare parametri alla procedura e magari non usare un oggetto?

Malgrado la soluzione di un oggetto di wrapper può tornare utile il passare dei parametri alla procedura chiamante, il che ci permette anche di non usare completamente un oggetto e di passare tutti i dati su cui deve lavorare il thread alla funzione che ne costituisce il main, vediamo come funziona

#include <iostream>
#include <thread>
#include <cstdlib>
#include <string>

using namespace std;

void stampa(string cosa, string altracosa){
 for(int a=0;a<10;a++){
 cout<< cosa << altracosa << endl;
 sleep(1);
 }
}

int main()
{
 std::thread t1(stampa, "bla bla bla", " yada yada yada");
 t1.join();
};

semplicemente accodiamo i parametri da passare alla funzione e ci affidiamo alle stl per la determinazione corretta del tipo ( la sintassi per gli argomenti multipli esisteva già, ma è stata migliorata, se siete curiosi leggete qui )

e per concludere passiamo alle questioni pratiche

L’ambiente utilizzato

Per l’ambiente utilizzato valgono le considerazioni fatte nel post precedente più qualcos’altro:
le funzioni lambda del C++0X sono incluse nei compilatori GNU a partire dalla 4.5 mentre la maggior parte dei sistemi linux attualmente in circolazione usa la 4.3, questa volta però non possiamo aggirare il problema installandocci  MinGW da windows, infatti sorgono altri problemi, i phread.
La libreria dei thread appena destritta usa pthread per creare e gestire i thread, e l’implementazione della libreria per windows non include pthread, quindi questa volta ci serve una vera Linux con un vero gcc ( oppure un implementazione commerciale per windows tipo questa che però non ho verificato, se qualcuno lo fa può scrivere sui commenti come funziona e se funziona e se ne vale la pena)

  • Anche lavorando con la 4.5 di default il compilatore non accetta la nuova sintassi, bisogna specificare come flag al g++ -std=c++0x
  • Inoltre bisogna aggiungere -lpthread per usare la lib dei pthread
  • se continua a non funzionare bisogna installare verificate che sia installato anche gcc-multilib
  • se ancora continua a non funzionare e vi da errori del tipo fatal error: asm/errno.h allora l’installatore non ha fatto il suo dovere e dovete correggere a mano il nome di una cartella:

andate su /usr/include/ e controllate che esista la cartella asm-generic, se esiste fatene un link software su /usr/include/asm (NON rinominatela, non si sa mai che può succedere con un aggiornamento di pacchetti ) se non esiste e non avete ancora risolto… mi dispiace ma non so che dirvi, non sono partito da un sistema vergine e quindi non ho verificato tutte le necessità del programma… scrivetemi nei commenti e cerchiamo una soluzione.

E per questo articolo è tutto, continuerò la prossima volta parlando della gestione delle risorse condivise e degli strumenti di sincronizzazione, la parte più divertente deve ancora arrivare :)

E voi lo sapevate che un articolo condiviso su facebook senza un immagine di anteprima viene cliccato molto meno?

chiudo con un immagine parzialmente appropriata per far fare l’anteprima a facebook :D

Lo spazio di indirizzamento di un Thread è interno a quello del processo padre, ma questo lo sapevate già, per il resto questa immagine potrebbe essere pure fuorviante visto che descrive dei thread software xD

Questo post fa parte di una serie, gli altri post riguardanti il Cpp0X sono:

Share Button