Passaggio di parametri ad un Thread in C++ 11

Nella lezione precedente abbiamo visto come creare thread in C++ 11 utilizzando l'oggetto std::thread. Tuttavia, ci siamo limitati a creare thread che non accettano parametri in ingresso e che non restituiscono nessun risultato.

In questa lezione andremo più nel dettaglio e vedremo come passare parametri ad un thread in C++ 11. In particolare vedremo che di default i parametri vengono passati per copia. Vedremo inoltre come passare parametri per riferimento e per move.

Thread e parametri

Abbiamo visto che il modo più semplice di creare un thread in C++ 11 è quello di creare un oggetto std::thread passando come parametro una funzione che verrà eseguita nel thread stesso.

Finora abbiamo utilizzato funzioni, o comunque oggetti callable, che non restituiscono nessun risultato e non accettano parametri in ingresso.

Nella maggior parte dei casi, tuttavia, sorge l'esigenza di dover passare dei parametri dal thread principale ai thread secondari creati.

Il passaggio di parametri ad un thread è possibile in modo molto semplice. Basta creare una funzione entry point che accetta i parametri necessari e passare questi parametri al costruttore dell'oggetto std::thread.

Proviamo a chiarire con un esempio:

#include <iostream>

void funzione_thread(int a, int b) {
    std::cout << "a + b = " << a + b << std::endl;
}

int main() {
    std::thread thread1(funzione_thread, 1, 2);

    thread1.join();

    return 0;
}

In questo esempio abbiamo creato una funzione entry point che accetta due parametri interi. Successivamente abbiamo creato un thread passando come parametro la funzione entry point e due interi.

Definizione

Passaggio di parametri ad un thread

Per passare parametri ad un oggetto std::thread è sufficiente passare come parametro la funzione entry point e i parametri necessari. La condizione necessaria è che l'entry point accetti i parametri passati.

La sintassi è la seguente:

void entry_point(tipo parametro_1, tipo parametro_2, ...) {
    /* .... */
}

std::thread t(entry_point, parametro_1, parametro_2, ...);

Dettagli sul Passaggio dei parametri

Nel descrivere come passare parametri ad un thread, tuttavia, abbiamo omesso un dettaglio molto importante: come vengono passati i parametri.

Un qualsiasi parametro passato al costruttore di un thread subisce due passaggi:

  1. Il parametro viene copiato nella memoria locale del thread;
  2. Il parametro copiato nella memoria locale viene successivamente passato alla funzione entry point come r-value, come se fosse un valore temporaneo.

Questo comportamento va sempre tenuto a mente, perché si possono ottenere risultati inaspettati anche quando la funzione di entry point si aspetta un riferimento.

Per comprendere meglio esaminiamo un primo esempio:

void funzione_thread(const std::string &s) {
    std::cout << "s = " << s << std::endl;
}

/* Creazione del thread */
std::thread t(funzione_thread, "Ciao");

Quello che accade in questo caso è che il parametro stringa letterale "Ciao" viene interpretato come const char *, ossia puntatore a caratteri. Quindi viene copiato il puntatore all'interno della memoria locale del thread. Successivamente, quando viene invocata la funzione funzione_thread dall'interno del thread stesso, avviene la conversione in std::string e viene passato come r-value.

Questo può avere effetti problematici quando si ha a che fare con le variabili automatiche, come nel caso seguente:

void funzione_thread(const std::string &s) {
    std::cout << "s = " << s << std::endl;
}

void funzione_principale() {
    char buffer[] = "Ciao";
    std::thread t(funzione_thread, buffer);
}

In questo caso viene passato il puntatore locale buffer al costruttore del thread che lo copia nella memoria locale. Successivamente, può accadere che la funzione funzione_principale termini prima che il thread venga eseguito, e quindi l'area di memoria che contiene la stringa "Ciao" venga liberata.

Per tal motivo, una soluzione è quella di effettuare il casting esplicito del parametro in std::string:

void funzione_thread(const std::string &s) {
    std::cout << "s = " << s << std::endl;
}

void funzione_principale() {
    char buffer[] = "Ciao";
    std::thread t(funzione_thread, std::string(buffer));
}

Il problema principale sta nel fatto che la conversione implicita di un parametro passato ad un thread può avvenire in un secondo momento rispetto alla creazione del thread stesso, e quindi non è possibile prevedere quando avverrà.

Definizione

Passaggio di parametri per copia

Di default, il comportamento del passaggio dei parametri ad un thread avviene per copia, ossia per valore.

In dettaglio, ciascun parametro viene copiato nella memoria locale del thread, e successivamente viene passato alla funzione entry point come r-value.

Passaggio di parametri per riferimento

Nell'esempio di sopra, la funzione entry point del thread accetta un parametro riferimento costante:

void funzione_thread(const std::string &s) {
    std::cout << "s = " << s << std::endl;
}

Abbiamo visto che, nonostante ciò, in realtà viene passata una copia locale del parametro.

In ogni caso, normalmente non si può passare un parametro come riferimento al costruttore di un thread. Questo perché il costruttore non conosce i tipi dei parametri che riceve, e copia in maniera bovina i parametri passati. Successivamente, il codice interno di std::thread passa i parametri copiati in locale come r-value alla funzione entry point. Pertanto, se omettiamo la parola chiave const nel parametro della funzione entry point, il compilatore ci segnalerà un errore:

void funzione_thread(std::string &s) {
    std::cout << "s = " << s << std::endl;
}

std::string p{"Ciao"};

std::thread t(funzione_thread, p);
std::thread arguments must be invocable after conversion to rvalues

La motivazione sta semplicemente nel fatto che un r-value non può essere passato per riferimento non costante.

L'unico modo di poter passare un parametro per riferimento, così che la funzione entry point possa modificarlo, è quello di utilizzare std::ref, indicando esplicitamente questa necessità:

void funzione_thread(std::string &s) {
    std::cout << "s = " << s << std::endl;
}

std::string p{"Ciao"};

std::thread t(funzione_thread, std::ref(p));
Definizione

Passaggio di parametri per riferimento ad un thread

Per passare un parametro per riferimento ad un thread in C++ 11 bisogna fare uso esplicito della funzione helper std::ref.

void f(tipo &p) {
    /* ... */
}

std::thread t(f, std::ref(p));

Passaggio di parametri per move

In alcuni casi il passaggio per riferimento oppure per copia non è possibile. Questo perché il parametro passato al costruttore del thread non è copiabile, e quindi non è possibile copiarlo in memoria locale.

Un possibile esempio è quello in cui vogliamo passare un oggetto std::unique_ptr ad un thread:

void funzione_thread(std::unique_ptr<int> p) {
    std::cout << "p = " << *p << std::endl;
}

Un oggetto std::unique_ptr non è copiabile, in quanto in ogni istante solo un'istanza di std::unique_ptr può puntatre ad un dato oggetto. Per questo motivo quando si assegna uno unique_ptr ad un altro, il primo viene invalidato e il secondo assume il suo valore.

Ora, finché si passa un valore temporaneo ad un thread, non ci sono problemi, perché il parametro viene passato per move:

std::thread t(funzione_thread, std::unique_ptr<int>(new int(42)));

Il problema sorge, invece, quando bisogna passare un parametro con un nome, ossia non temporaneo:

std::unique_ptr<int> p(new int(42));

std::thread t(funzione_thread, p);

Questo esempio non compila, perché il compilatore non riesce a capire come passare il parametro p al costruttore del thread dato che non può essere copiato.

In questi casi l'unico modo per poter passare il parametro è quello di fare uso esplicito di std::move:

std::unique_ptr<int> p(new int(42));

std::thread t(funzione_thread, std::move(p));

In questo modo la proprietà, l'ownership, dell'oggetto viene passata al thread, e quindi il parametro p viene invalidato.

Definizione

Passaggio di parametri per move ad un thread

Quando si vuole passare un parametro per move ad un thread in C++ 11 bisogna fare uso esplicito della funzione helper std::move.

void f(tipo &&p) {
    /* ... */
}

std::thread t(f, std::move(p));

In Sintesi

In questa lezione abbiamo esteso il modo con cui possiamo creare un thread in C++ 11. In particolare abbiamo visto come passare argomenti ad un thread. Gli argomenti passati verranno, poi, passati alla funzione entry point del thread che, quindi, deve avere un numero di argomenti uguale a quelli passati al costruttore del thread.

Abbiamo visto, inoltre, che tutti i parametri vengono copiati all'interno della memoria locale del thread e, successivamente, passati come r-value alla funzione entry point.

Se vogliamo passare parametri per riferimento oppure per move, dobbiamo fare uso esplicito delle funzioni helper std::ref e std::move.