multithreading (tsz. multithreadings)
A multithreading (többszálú programozás) egy olyan programozási technika, amely lehetővé teszi, hogy egy program több végrehajtási szálon (thread) fusson párhuzamosan. A C++ szabványos könyvtára (STL) tartalmazza a std::thread osztályt, amely segít a szálkezelésben. Ebben a cikkben bemutatjuk a multithreading alapjait, a szinkronizálás módjait és néhány gyakori problémát.
Egy szál egy olyan független végrehajtási egység, amely egy adott programon belül fut. Egy program futhat egyetlen szálon (single-threaded) vagy több szálon (multi-threaded).
Példa egy egyszálú programra:
#include <iostream>
void szamolas() {
for (int i = 0; i < 5; i++) {
std::cout << "Számolás: " << i << std::endl;
}
}
int main() {
szamolas();
return 0;
}
Ebben az esetben a main() függvény egyetlen végrehajtási útvonalon fut.
A C++11 bevezetésével az std::thread osztály lehetővé teszi több szál létrehozását és kezelését.
Példa két szál futtatására:
#include <iostream>
#include <thread>
void szamolas() {
for (int i = 0; i < 5; i++) {
std::cout << "Számolás: " << i << std::endl;
}
}
int main() {
std::thread szal(szamolas); // Létrehozunk egy új szálat
szal.join(); // Megvárjuk, hogy a szál befejezze a munkáját
std::cout << "Főprogram vége." << std::endl;
return 0;
}
Ebben a programban: - std::thread szal(szamolas); → Létrehozunk egy új szálat, amely a szamolas
függvényt hajtja végre. - szal.join(); → A főprogram vár, amíg a szál befejezi a futását.
Ha több szálat szeretnénk indítani, minden egyes szálhoz külön std::thread
példányt hozunk létre.
Példa több szál indítására:
#include <iostream>
#include <thread>
void szamolas(int id) {
for (int i = 0; i < 5; i++) {
std::cout << "Szál " << id << ": " << i << std::endl;
}
}
int main() {
std::thread szal1(szamolas, 1);
std::thread szal2(szamolas, 2);
szal1.join();
szal2.join();
std::cout << "Főprogram vége." << std::endl;
return 0;
}
Itt két különálló szál fut, és mindkettő párhuzamosan végrehajtja a szamolas
függvényt.
A párhuzamos végrehajtás versenyhelyzeteket (race condition) hozhat létre, ha több szál próbálja módosítani ugyanazt az erőforrást.
#include <iostream>
#include <thread>
int szamlalo = 0;
void novel() {
for (int i = 0; i < 1000; i++) {
szamlalo++; // Versenyhelyzet lehetősége
}
}
int main() {
std::thread szal1(novel);
std::thread szal2(novel);
szal1.join();
szal2.join();
std::cout << "Számláló értéke: " << szamlalo << std::endl; // Nem feltétlenül lesz 2000!
return 0;
}
A fenti kód nem garantálja, hogy a számláló értéke 2000 lesz, mert a két szál egyszerre próbálja módosítani az értéket.
A std::mutex
segít megakadályozni a versenyhelyzeteket.
#include <iostream>
#include <thread>
#include <mutex>
int szamlalo = 0;
std::mutex mtx;
void novel() {
for (int i = 0; i < 1000; i++) {
mtx.lock();
szamlalo++;
mtx.unlock();
}
}
int main() {
std::thread szal1(novel);
std::thread szal2(novel);
szal1.join();
szal2.join();
std::cout << "Számláló értéke: " << szamlalo << std::endl;
return 0;
}
Itt a mutex (mtx) biztosítja, hogy egyszerre csak egy szál férjen hozzá a szamlalo
változóhoz.
A deadlock egy olyan helyzet, amikor két vagy több szál vár egy erőforrásra, és egyik sem tud továbbhaladni.
Példa deadlockra:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx1, mtx2;
void f1() {
mtx1.lock();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
mtx2.lock(); // Itt deadlock léphet fel
std::cout << "f1 fut" << std::endl;
mtx2.unlock();
mtx1.unlock();
}
void f2() {
mtx2.lock();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
mtx1.lock(); // Itt deadlock léphet fel
std::cout << "f2 fut" << std::endl;
mtx1.unlock();
mtx2.unlock();
}
int main() {
std::thread t1(f1);
std::thread t2(f2);
t1.join();
t2.join();
return 0;
}
A fenti kód elakadhat, mert mindkét szál vár a másik által lezárt erőforrásra.
Megoldás: std::lock() függvény használata, amely egyszerre próbál több mutexet lezárni.
A std::condition_variable
segít, ha egy szál várakozni szeretne egy eseményre.
Példa:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool kesz = false;
void dolgozo() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, { return kesz; });
std::cout << "Munka elindult!" << std::endl;
}
void vezerlo() {
std::this_thread::sleep_for(std::chrono::seconds(1));
{
std::lock_guard<std::mutex> lock(mtx);
kesz = true;
}
cv.notify_one();
}
int main() {
std::thread t1(dolgozo);
std::thread t2(vezerlo);
t1.join();
t2.join();
return 0;
}
Itt a cv.wait()
megvárja, amíg a vezerlo
szál beállítja a kesz változót.
A multithreading nagy teljesítményt nyújt, de megfelelő szinkronizáció nélkül versenyhelyzetek és deadlockok alakulhatnak ki. A std::mutex, std::condition_variable és egyéb szinkronizációs eszközök segítenek a stabil kód megírásában.