Creazione di un Thread in C++ 11

Alla base della concorrenza in C++ 11 c'è la possibilità di creare thread multipli, che possono essere eseguiti in maniera concorrente rispetto al thread principale. In questa lezione introduttiva, impareremo come creare un thread in C++ 11, utilizzando una funzione, un oggetto callable o una lambda.

Vedremo anche come utilizzare il metodo join per attendere la terminazione di un thread.

Concetti Chiave
  • Per creare un thread in C++ 11 è necessario utilizzare la classe std::thread e il suo costruttore;
  • Il costruttore accetta come argomento una funzione di entry point;
  • La funzione di entry point non deve restituire nessun valore, quindi deve essere di tipo void;
  • Appena creato, un oggetto std::thread comincia la sua esecuzione;
  • Per attendere la terminazione di un thread è necessario utilizzare il metodo join.

Creazione di un Thread

Ciascun programma C++ ha almeno un thread attivo, chiamato thread principale, che viene eseguito dal sistema operativo. Il thread principale esegue il codice che è stato scritto nella funzione main().

A partire da questo thread è possibile lanciare thread multipli, che possono essere eseguiti in maniera concorrente. In generale, per creare un thread è necessario creare una funzione che rappresenta l'entry point, ossia il punto di partenza dell'esecuzione del thread.

Successivamente il thread termina quando la funzione di entry point termina.

Per creare un thread è necessario utilizzare la classe std::thread e il suo costruttore. Il costruttore accetta come argomento la funzione di entry point. Per poter utilizzare la classe std::thread è necessario includere il file header di libreria <thread>.

#include <iostream>
#include <thread>

void funzione_thread() {
    /* Svolge le operazioni del thread */
}

int main() {
    /* Crea ed avvia un thread usando la funzione funzione_thread */
    std::thread thread1(funzione_thread);

    /* Il thread principale svolge altre attività */

    /* Attende la terminazione del thread */
    thread1.join();

    return 0;
}

Questo esempio mostra il caso più semplice di creazione di un thread. Abbiamo utilizzato una funzione che non accetta nessun parametro e non restituisce alcun valore per creare il thread che svolgerà le proprie operazioni in maniera concorrente rispetto al thread principale.

Generalmente struttura di una funzione che funge da entry point è la seguente:

void funzione_thread() {
    /* Svolge le operazioni del thread */
}

In altre parole, la funzione restituisce void, cioè non restituisce alcun valore. La funzione può, tuttavia, accettare parametri. Il passaggio di parametri alla funzione lo vedremo, però, nelle prossime lezioni.

Successivamente, sempre nel main del nostro programma, abbiamo utilizzato il metodo join dell'oggetto thread per attendere che il thread termini. Il metodo join è bloccante, ossia il thread principale attende che il thread termini prima di proseguire con l'esecuzione del codice successivo.

Definizione

Creazione di un thread in C++ 11

Nello standard C++11, la creazione di un nuovo thread avviene creando un oggetto della classe std::thread e passando alla funzione costruttore la funzione di entry point.

La classe è definita nell'header <thread>. La funzione di entry point non deve restituire nessun valore, quindi deve essere di tipo void.

La sintassi è la seguente:

std::thread thread1(funzione_thread);

L'avvio del thread è automatico, in altre parole il thread comincia la propria esecuzione non appena viene creato.

Entry point di un thread

Nello standard C++11 possiamo utilizzare anche altre entità per creare un thread. In particolare, possiamo utilizzare:

  • Un oggetto callable;
  • Una lambda.

Nel primo caso, anziché passare una funzione al costruttore di std::thread, possiamo passare un oggetto callable. Un oggetto callable è un oggetto che implementa l'operatore di invocazione ().

Prendiamo un esempio:

class OggettoCallable {
public:
    void operator()() {
        /* Svolge le operazioni del thread */
    }
};

In questo caso abbiamo un oggetto OggettoCallable che, implementando l'operatore di invocazione (), può essere invocato come una funzione. Ad esempio, possiamo invocare un'istanza dell'oggetto in questo modo:

OggettoCallable o;

/* Invocazione */
o();

Dal momento che può essere invocato come una funzione, possiamo passare l'oggetto al costruttore di un thread, in questo modo:

std::thread thread1(o);

Quando creiamo il thread in questo modo, bisogna fare attenzione al fatto che l'oggetto o viene copiato all'interno del thread. Quindi bisogna assicurarsi che la copia dell'oggetto o si comporti come l'oggetto originale per evitare effetti indesiderati, magari implementando il costruttore di copia.

La seconda possibilità è quella di utilizzare una lambda, ossia una funzione anonima. Ad esempio, possiamo creare un thread in questo modo:

std::thread thread1([]() {
    /* Svolge le operazioni del thread */
});
Definizione

Entry point di un Thread in C++ 11

Nello standard C++ 11, l'entry point di un thread può essere:

  • Una funzione che restituisce void;

    void f() {
        /* Operazioni */
    }
    
    std::thread t(f);
    
  • Un oggetto callable, ossia un oggetto in cui è implementato l'operatore di invocazione ();

    class OggettoCallable {
    public:
        void operator()() {
            /* Operazioni */
        }
    };
    
    OggettoCallable o;
    std::thread t(o)
    
  • Una lambda;

    std::thread t([]() {
        /* Operazioni */
    });
    

Metodo di una classe come entry point di un Thread

Esiste un'ulteriore possibilità per creare un thread. Supponiamo vogliamo usare come entry point di un thread il metodo di un oggetto.

Ad esempio, vogliamo usare il metodo esempio dell'oggetto p di classe Prova:

class Prova {
public:
    void esempio() {
        /* Operazioni */
    }
};

Prova p;

L'oggetto p non è di una classe callable, non può essere invocato.

Pertanto lo standard C++ 11 mette a disposizione la facility std::bind che permette di creare un oggetto callable a partire da un metodo di una classe.

La sintassi diventa:

std::thread t(std::bind(&Prova::esempio, &p));

In questo caso, il primo parametro di std::bind è l'indirizzo del metodo esempio della classe Prova. Il secondo parametro è l'indirizzo dell'oggetto p.

Definizione

Metodo di una classe come entry point di un Thread in C++ 11

Nello standard C++ 11, un metodo di un oggetto può essere usato come entry point di un thread utilizzando la facility std::bind.

La sintassi è la seguente:

class C {
public:
    void m() {
        /* Operazioni */
    }
};

C c;

std::thread t(std::bind(&C::m, &c));

Attesa della terminazione di un thread

Una volta che un thread è stato creato, il thread principale può continuare con l'esecuzione del codice. In questo caso, il thread principale e il thread creato possono eseguire le proprie operazioni in maniera concorrente.

Tuttavia, quando il thread principale termina le proprie operazioni bisogna assicurarsi che il thread creato termini le proprie operazioni prima di terminare il programma. In caso contrario, se il thread principale termina prima del thread secondario, quest'ultimo invocherà la funzione std::terminate() che termina il programma.

A questo punto si può utilizzare il metodo join dell'oggetto thread per attendere che il thread termini. Il metodo join è bloccante, ossia il thread principale attende che il thread termini prima di proseguire con l'esecuzione del codice successivo.

#include <iostream>

void funzione_thread() {
    /* Svolge le operazioni del thread */
}

int main() {
    /* Crea ed avvia un thread usando la funzione funzione_thread */
    std::thread thread1(funzione_thread);

    /* Il thread principale svolge altre attività */

    /* Attende la terminazione del thread */
    thread1.join();

    /* Da questo punto in poi il thread principale è l'unico thread attivo */

    return 0;
}

Il metodo join rimane in attesa della terminazione del thread secondario, se quest'ultimo è ancora in esecuzione, oppure esce immediatamente se il thread secondario è già terminato.

Quando si invoca la join tutte le risorse locali del thread vengono rilasciate. In particolare, se il thread secondario ha creato un oggetto locale, questo oggetto viene distrutto quando il thread termina.

Inoltre, invocare due volte il metodo join su un thread genera un'eccezione di tipo std::system_error. Per evitare questo problema, un oggetto di tipo std::thread mette a disposizione il metodo joinable che restituisce true se il thread è ancora in esecuzione oppure se è in attesa di essere terminato, altrimenti restituisce false.

Analizziamo l'esempio che segue:

#include <iostream>

void funzione_thread() {
    /* Svolge le operazioni del thread */
}

int main() {
    /* Crea ed avvia un thread usando la funzione funzione_thread */
    std::thread thread1(funzione_thread);

    /* Il thread principale svolge altre attività */

    std::cout << "Thread joinable: " << thread1.joinable() << std::endl;

    /* Attende la terminazione del thread */
    thread1.join();

    /* Da questo punto in poi il thread principale è l'unico thread attivo */

    std::cout << "Thread joinable: " << thread1.joinable() << std::endl;

    return 0;
}

Se proviamo ad eseguire questo programma, otteniamo il seguente output:

Thread joinable: 1

Thread joinable: 0

Il fatto che la prima stampa mostra 1 ci dice che il thread o è ancora in esecuzione oppure che è in attesa di una join. La seconda stampa invece mostra 0 che ci dice che il thread è terminato correttamente.

Definizione

Attesa di un Thread in C++ 11 - join

Nello standard C++ 11, per attendere la terminazione di un thread si utilizza il metodo join dell'oggetto std::thread. Il metodo join è bloccante, ossia il thread principale attende la terminazione prima di riprendere la propria esecuzione.

La sintassi è la seguente:

std::thread t(/* ... */);

/* ... */

t.join();

Invocare due volte il metodo join su un thread genera un'eccezione di tipo std::system_error.

Definizione

joinable e Thread

Il metodo joinable dell'oggetto std::thread restituisce true nei seguenti casi:

  • il thread è ancora in esecuzione e non è stata invocata la join;
  • il thread è in attesa di essere terminato tramite la join.

Restituisce false in caso contrario.

In Sintesi

In questa lezione abbiamo visto gli strumenti base messi a disposizione dallo standard C++ 11 per la creazione di thread.

Abbiamo visto come creare un thread utilizzando l'oggetto std::thread e il suo costruttore che accetta come parametro una funzione entry point.

Abbiamo visto come passare altri tipi di entry point all'oggetto thread tra cui:

  • Oggetti callable;
  • funzioni lambda;
  • funzioni membro di una classe.

Infine abbiamo visto come attendere la terminazione di un thread attraverso il metodo join della classe thread.