Paralelno izvršavanje koda

Kada dio programa želimo izvršiti paralelno u posebnoj dretvi zatvorimo ga u funkciju (funkcijski objekt ili lambdu) i funkciju izvršimo kroz std::async().

Funkcija std::async() je definirana u zaglavlju <future> i uzima objekt koji se može pozvati kao funkcija f te argumente koje f zahtjeva. Ako je moguće funkcijski objekt f će biti izvršen u posebnoj dretvi, paralelno s glavnom dretvom programa.

Ako f vraća neku vrijednost std::async() će tu vrijednost vratiti kroz std::future<> objekt.

Primjer. Pretpostavimo da imamo dvije funkcije f1() i f2() koje vraćaju rezultat tipa int i da nas zanima suma povratnih vrijednosti tih funkcija:

int rez = f1() + f2(); // sekvencijalno izvršavanje

Paralelna verzija koda je dana ovdje:

future<int> fut{async(f1)}; // Izvršava se u posebnoj dretvi (eventualno).
int i2 = f2();
int i1 = fut.get();       // Čekaj da posebna dretva završi i preuzmi rezultat
rez = i1 + i2;

std::async() i std::future<>

Ako imamo funkciju koja vraća reziltat tipa T

T f();

koju želimo izvršiti u zasebnoj dretvi, paralelno s glavnom dretvom, onda ju predajemo funkcji std::async() (zaglavlje <future>):

  • U pozivu auto fut = std::async(f); funkcija std::async() pokreće funkciju f() u (eventualno) novoj dretvi i odmah vraća objekt tipa std::future<T> (zaglavlje <future>).

  • Objekt fut (tipa std::future<T>) predstavlja budući rezultat izvršavanja funcije f(). Taj rezultat dobivamo pozivanjem metode get() na fut objektu. Poziv T rez = fut.get() (u glavnoj dretvi) blokira glavnu dretvu sve dok rezultat nije spreman. Tada get() vraća povratnu vrijednost funkcije f().

  • Kod koji želimo da se izvršava paralelno s kodom funkcije f() stavljamo između poziva auto fut = std::async(f); i poziva T rez = fut.get().

Ako funkcija f() ne vraća ništa (T = void) onda je povratna vrijednost od std::async() tipa std::future<void> i metoda std::future<void>::get() ne vraća ništa već samo čeka da f() završi.

Tri načina ponašanja funkcije std::async()

Moguća je situacija u kojoj std::async() u naredbi auto fut = std::async(f); ne može kreirati novu dretvu. Tada će poziv funkcije f() biti odgođen do poziva T rez = fut.get(). Program se u tom slučaju odvija serijski.

Moguće je eksplicitno odrediti ponašanje funkcije std::async() dodavanjem novog parametra:

  • auto fut = std::async(std::launch::async, f); Pokreni funkciju f() u posebnoj dretvi. U slučaju da se dretva ne može kreirati izbacuje izuzetak std::system_error.

  • auto fut = std::async(std::launch::deferred, f); Pokreni funkciju f() serijski (u glavnoj dretvi) kada bude zatražen rezultat, dakle u pozivu T rez = fut.get().

  • Kada ne navedmo prvi argument, auto fut = std::async(f); tada std::async() pokušava kreirati novu dretvu i ako ne uspije prelazi na ponašanje određeno sa std::launch::deferred.

Preporuka je ne koristiti eksplicitno std::launch::async i prepustiti sustavu da odredi može li se kreirati nova dretva ili će se kod izvršiti serijski.

Funkciju std::future<>::get() smijemo pozvati samo jednom. Nakon tog poziva std::future<> objekt nije u ispravnom stanju i sljedeći poziv get() metode daje grešku pri izvršavanju. Stoga std::future<> ima metodu valid() koja vraća true ako matoda get() još nije pozvana.

if(fut.valid())  // Provjerava je li future u ispravnom stanju
      fut.get();

Povratna vrijednost ili izuzetak

Kod koji izvršavamo paralelno može izbaciti izuzetak. U tom će slučaju metoda std::future<void>::get() izbaciti (proslijediti) taj izuzetak.

double * f(){
    double *p = nullptr;
    long limit = std::numeric_limits<long>::max();
    p = new double[limit];  // Izbacuje izuzetak
    return p;
}

int main(){
    auto fut{async(f)};
    // ... paralelan kod
    try{
        double * p = fut.get();
    }
    catch(const exception & e){
        cerr << "Exception: " << e.what() << endl;
    }
    return 0;
}

Čekanje na dretvu

std::future<>::wait()

Moguće je čekati da funkcija pokrenuta s std::async() završi bez procesiranja rezultata. To omogućava funkcija std::future<>::wait() koja se za razliku od std::future<>::get() može pozvati više puta. Funkcija std::future<>::wait() će pokrenuti izvršavanje funkcije ako je bilo odgođeno. Da bismo dobili rezultat izvršavanja moramo zvati std::future<>::get().

std::future<>::wait_for() i std::future<>::wait_until()

Ove dvije funkcije čekaju da dretva završi određeni vremenski interval ili do određenog trenutka. One neće startati dretvu ako je odgođena. Vraćaju jednu od tri vrijednosti:

  • std::future_status::deferred ako je async() nije startao funkciju;

  • std::future_status::timeout ako je funkcija startana u dretvi ali nije gotova;

  • std::future_status::ready ako je funkcija završila.

Prijenos argumenata

Funkcija std::async() nakon funkcijskog objekta (funkcije ili lambde) uzima proizvoljan broj parametara koje predaje funkcijskom objektu koji izvršava. Broj argumenata je proizvoljan i svi se prenose po vrijednosti.

double * f(size_t n, double a){
    double *p = nullptr;
    p = new double[n];
    fill(p, p+n, init);
    // ....
    return p;
}

int main(){
    auto fut = async(f, 100000, 3.14);
    // ... paralelan kod
    double * p = fut.get();
    // ...
    delete [] p;
    return 0;
}

Prijenos po referenci

Za prijenos po referenci moramo koristiti std::ref (zaglavlje <functional>).

double * f(size_t n, double & a){
     // ....
    return p;
}

int main(){
    double a = 17.0;
    auto fut = async(f, 100000, std::ref(a));
    // ... paralelan kod
    double * p = fut.get();
    // ...
    delete [] p;
    return 0;
}

Imenik std::this_thread

Daje sljedeće funkcije (zaglavlje <thread>):

  • this_thread::get_id() daje ID trenutne dretve

  • this_thread::sleep_for(dur) blokira dretvu za trajanja dur

  • this_thread::sleep_until(tp) blokira dretvu sve do trenutka tp

  • this_thread::yield() sugerira prepuštanje procesora sljedećoj dretvi

Klasa std::thread

Klasa std::thread pripada sučelju niske razine. Slična je donekle funkciji std::async(). Klasa std::thread:

  • isto kao i std::async(), konstruktor klase std::thread uzima funkcijski objekt i argumente koje mu treba predati te odmah starta funkcijski objekt u posebnoj dretvi (ili izbacuje std::system_error);

  • kao i std::async(), konstruktor klase std::thread može uzeti funkciju, funkcijski objekt i lambdu;

  • svi argumeni koji se prenose funkciji predaju se po vrijednosti ili se treba koristiti std::ref();

  • nema ugrađenog mehanizma za povrat vrijednosti u pozivnu dretvu;

  • ne može vratiti izuzetak. To znači da mora procesirati sve svoje izuzetke, neuhvaćeni izuzetak završava čitav program;

  • svaka dretva ima jedinstveni ID koji se na std::thread objektu dobiva pomoću get_id() metode;

  • pozivni program treba na dretvi pozvati join() ako želi čekati da dretva završi, ili detach() ako želi da dretva nastavi raditi u pozadini.

void doSomething();

std::thread t(doSomething); // starta doSomething() u pozadini
// ...
t.join(); // čekaj da doSomething() završi

std::promise<>

Objekt tipa std::promise<T> služi za komunikaciju vrijednosti tipa T iz jedne dretve u drugu. Umjesto vrijednosti može držati izuzetak ako je izračunavanje vrijednosti završilo izbacivanjem izuzetka.

Metode:

  • set_value(t) - postavi vrijednost na t;

  • set_exception(e) - spremi izuzetak e (tip exception_ptr). Izuzetak koji je izbačen može se dobiti sa current_exception() (zaglavlje <stdexcept>).

  • get_future() - vrati future<T> objekt vezan uz dijeljeno stanje.

Kada dretva stavi vrijednost u std::promise<T> objekt (podatak ili izuzetak) pripadni future<T> objekt postaje spreman i dretva koja čeka na njemu dobiva vrijednost (ili izuzetak) i nastavlja izvršavanje.