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>):

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:

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>):

Klasa std::thread

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

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:

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.