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()
nafut
objektu. 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::deferred
ako jeasync()
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 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::thread
uzima 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::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ć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.