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);funkcijastd::async()pokreće funkcijuf()u (eventualno) novoj dretvi i odmah vraća objekt tipastd::future<T>(zaglavlje<future>). -
Objekt
fut(tipastd::future<T>) predstavlja budući rezultat izvršavanja funcijef(). Taj rezultat dobivamo pozivanjem metodeget()nafutobjektu. PozivT rez = fut.get()(u glavnoj dretvi) blokira glavnu dretvu sve dok rezultat nije spreman. Tadaget()vraća povratnu vrijednost funkcijef(). -
Kod koji želimo da se izvršava paralelno s kodom funkcije
f()stavljamo između pozivaauto fut = std::async(f);i pozivaT 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 funkcijuf()u posebnoj dretvi. U slučaju da se dretva ne može kreirati izbacuje izuzetakstd::system_error. -
auto fut = std::async(std::launch::deferred, f);Pokreni funkcijuf()serijski (u glavnoj dretvi) kada bude zatražen rezultat, dakle u pozivuT rez = fut.get(). -
Kada ne navedmo prvi argument,
auto fut = std::async(f);tadastd::async()pokušava kreirati novu dretvu i ako ne uspije prelazi na ponašanje određeno sastd::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::deferredako jeasync()nije startao funkciju; -
std::future_status::timeoutako je funkcija startana u dretvi ali nije gotova; -
std::future_status::readyako 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 trajanjadur -
this_thread::sleep_until(tp)blokira dretvu sve do trenutkatp -
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 klasestd::threaduzima funkcijski objekt i argumente koje mu treba predati te odmah starta funkcijski objekt u posebnoj dretvi (ili izbacujestd::system_error); -
kao i
std::async(), konstruktor klasestd::threadmož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::threadobjektu dobiva pomoćuget_id()metode; -
pozivni program treba na dretvi pozvati
join()ako želi čekati da dretva završi, ilidetach()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 nat; -
set_exception(e)- spremi izuzetake(tipexception_ptr). Izuzetak koji je izbačen može se dobiti sacurrent_exception()(zaglavlje<stdexcept>). -
get_future()- vratifuture<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.