C++11

Párhuzamosság

A C++ 11-ben mutattak be egy új könyvtárat, ami a szálak (threadek) támogatását foglalja magába. A könyvtár segítségével lehetőségünk van új szál indítására és több eszközt is tartalmaz a szinkronizációra, mint például mutexek, lockok és atomi változók.

Új szál indítása

A könyvtár segítségével könnyen tudunk új szálat indítani. Amikor létrehozunk egy std::thread példányt, akkor egy új szál automatikusan el is indul. Egy új szál indításánál meg kell mondanunk, hogy milyen kódot szeretnénk rajta futtatni. pl:

#include #include void helloWorld() { std::cout << ”Hello World” << std::endl; } int main() { std::thread t1(helloWorld); t1.join(); return 0; }

Minden szálhoz kapcsolódó eszközt a thread headerben találunk meg. A példában látható join függvény, akkor kényszeríti az aktuális szálat, hogy várja be a másikat. Jelen esetben a main szálat kényszeríti, hogy várja be a t1 szál befejeződését. Abban az esetben, ha nem hívjuk meg a join-t, a végeredmény kiszámíthatatlan lesz. Lehet hogy kiírja a Hello World üzenetet újsorral, lehet hogy új sor nélkül írja ki, de az is előfordulhat, hogy nem ír ki semmit, abban az esetben, ha a main hamarabb kilép, mint hogy a t1 szál befejeződjön.

Szálak azonosítása

Minden szál rendelkezik egy egyedi azonosítóval, egy id-val. Az std::thread osztályban megtalálható a get_id() függvény, amely segítségével lekérhetjük az adott szál azonosítóját. Az aktuális szálra könnyedén szerezhetünk referenciát az std::this_thread segítségével.

#include #include #include void helloWorld(){ std::cout << "Hello World " << std::this_thread::get_id() << std::endl; } int main(){ std::vector threads; for(int i = 0; i < 5; ++i){ threads.push_back(std::thread(helloWorld)); } for(auto& thread : threads){ thread.join(); } return 0; }

Ebben az esetben öt szálat indítunk, amelyekben a helloWorldöt hívjuk meg, ami kiírja a Hello World szöveget, majd a szál id-ját. A végeredmény megjósolhatatlan, hiszen nem tudjuk meghatározni, hogy milyen sorrendben fussanak le a szálak.

Új szál indítása lambda segítségével

Abban az esetben, ha a végrehajtandó kód rövid, mint az előző példánk esetében is, nem érdemes külön függvényt csinálni hozzá, hanem használhatunk lambda kifejezést. Az előző példa lambda kifejezés segítségével:

#include #include #include int main(){ std::vector threads; for(int i = 0; i < 5; ++i){ threads.push_back(std::thread([](){ std::cout << "Hello World " << std::this_thread::get_id() << std::endl; })); } for(auto& thread : threads){ thread.join(); } return 0; }

Szinkronizáció

Ha megosztott objektumokat szeretnénk használni több szálon, akkor jön képbe a szinkonizáció. Vegyünk példának egy egyszerű számlálót, amelyet több szálból is növelünk. Az eremény megjósolhatatlan lesz, ugyanis egy növelés három műveletből áll: kiolvassuk az értékét, ezt megnöveljük, majd visszaírjuk az új értéket. Ha a kiolvasás és eredmény visszaírás között egy másik szál is kiolvassa az értéket, akkor máris elromlik az eredmény.

A problémára több megoldás is létezik:

Egy speciális szemafor, a mutex segítségével megoldhatjuk a problémát, hiszen a mutexen egyszerre csak egy szál tud lockolni.

Mutex

A C++ 11 threading könyvtárában a mutexek a mutex headerben találhatóak, és egy mutexet az std::mutex osztály reprezentál. A mutex két fontos művelete a lock() és unlock(). A lock metódus blokkoló metódus, azaz csak akkor fog visszatérni, ha sikerült a mutexet lockolni. Egy számláló objektum mutexxel javítva:

struct Counter { std::mutex mutex; int value; Counter() : value(0) {} void increment(){ mutex.lock(); ++value; mutex.unlock(); } };

A try_lock segítségével csak megpróbálja lockolni, és ha sikerül igaz értékkel tér vissza, ha pedig nem, akkor hamissal.

Kivételek

Bővítsük ki a számlálónkat egy csökkentő metódussal, amely kivételt fog dobni, ha a számláló nulla. Ezzel az lesz a probléma, hogy ha a mutex unlockolása előtt dobom a kivételt, akkor a mutexet nem fogja unlockolni, így a program blokkolva lesz. Ez a probléma megoldható egy try-catch blokk bevezetésével, de ebben az esetben a kód kevésbé lesz már átlátható.

Automatikus lockolás

Arra az esetre, ha egy egész blocknyi kódot akarunk lock-kal védeni létezik egy eszköz, amely megóv minket az unlockolás elfelejtésétől/elmaradásától. Ez az eszköz az std::lock_guard.

Ez az osztály egy lock felügyelő, amely ha létrejön automatikusan lockolja a mutexet és amikor megsemmisül unlocoklja azt. Egy egyszerű példa erre:

struct ConcurrentSafeCounter { std::mutex mutex; Counter counter; void increment(){ std::lock_guard guard(mutex); counter.increment(); } void decrement(){ std::lock_guard guard(mutex); counter.decrement(); } };

Ebben az esetben nem kell az unlockolással törődnünk, mert ezt elvégzi a lock_guard destruktora.

Rekurzív lockolás

Abban az esetben, ha az objektum két olyan műveletet tartalmaz amelyek lockolni szeretnék a mutexet, legyenek mul és div. Legyen a harmadik metódusa a következő:

void both(int x, int y){ std::lock_guardmutex> lock(mutex); mul(x); div(y); }

Ha meghívjuk a both metódust, akkor a programunk sohasem fog terminálni, mert dead lock állapot fog fellépni. Ugyanis alapból egy szál nem lockolhatja kétszer ugyanazt a mutexet.

Erre létezik egy egyszerű megoldás, az std::recursive_mutex. Ezt a mutexet egy szál többször is lockolhatja, így az előbbi kód ennek segítségével már működni fog.

Időzített lockolás

Arra az esetre, ha nem akarjuk, hogy egy szál túl sokat várjon egy lockolásra lehetőségünk van időlimitet beállítani az std::timed_mutex és az std::recursive_timed_mutex segítségével.

Ezek a mutexek rendelkeznek az std::mutex minden műveletével, de ezeken kívül van még két fontos műveletük, a try_lock_for() és a try_lock_until().

A try_lock_for segítségével beállíthatunk egy időlimitet, ami után a függvény automatikusan visszatér. Igaz értékkel tér vissza, ha sikerült lockolni és hamissal, ha nem sikerült.

std::timed_mutex mutex; void work(){ std::chrono::milliseconds timeout(100); while(true){ if(mutex.try_lock_for(timeout)){ std::cout << std::this_thread::get_id() << ": do work with the mutex" << std::endl; std::chrono::milliseconds sleepDuration(250); std::this_thread::sleep_for(sleepDuration); mutex.unlock(); std::this_thread::sleep_for(sleepDuration); } else { std::cout << std::this_thread::get_id() << ": do work without mutex" << std::endl; std::chrono::milliseconds sleepDuration(100); std::this_thread::sleep_for(sleepDuration); } } } int main(){ std::thread t1(work); std::thread t2(work); t1.join(); t2.join(); return 0; }

A C++ 11 egy új szolgáltatása, az idő deklarálása. Hozzáférhetünk óra, perc, másodperc, ezredmásodperchez. Az std::this_thread::sleep_for az aktuális szálat altatja el a megadott ideig.

Once

Az std::call_once függvény segítségével lehetőségünk van egy kódot, csak egyszer végrehajtani, függetlenül attól, hogy hány szálból hívjuk meg.

std::once_flag flag; void do_something(){ std::call_once(flag, [](){std::cout << "Called once" << std::endl;}); std::cout << "Called each time" << std::endl; } int main(){ std::thread t1(do_something); std::thread t2(do_something); std::thread t3(do_something); std::thread t4(do_something); t1.join(); t2.join(); t3.join(); t4.join(); return 0; }

Minden call_once a once_flag-hez van kötve, így ez a rész csak egyszer fog lefutni.