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.
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:
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.
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.
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.
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:
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.
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:
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.
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ó.
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:
Ebben az esetben nem kell az unlockolással törődnünk, mert ezt elvégzi a lock_guard destruktora.
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ő:
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.
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.
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.
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.
Minden call_once a once_flag-hez van kötve, így ez a rész csak egyszer fog lefutni.