A mutex (a mutual exclusion, azaz kölcsönös kizárás rövidítése) egy olyan szinkronizációs eszköz a többszálú programozásban, amely lehetővé teszi, hogy egy megosztott erőforrást egyszerre csak egy szál használjon egy adott kritikus szakaszban.

Ha több szál megfelelő szinkronizáció nélkül fér hozzá ugyanahhoz a megosztott adathoz, és legalább az egyik szál módosítja azt, akkor C++-ban adatverseny (data race) alakulhat ki. Ez nem definiált működéshez vezethet.

A mutex úgy működik, mint egy zár: az a szál, amelyik éppen dolgozni akar az adattal, “lezárja” a mutexet, a többi szál pedig várakozni kényszerül, amíg a zár fel nem szabadul.

Analógia

Képzelj el egy egyfős mosdót egy irodában.

  1. Bemegy valaki a mosdóba és magára zárja az ajtót (mutex lezárása: lock).
  2. Amíg bent van, senki más nem tud bemenni. Ha más is szeretné használni a mosdót, _várakozik az ajtó előtt.
  3. Amikor a bent lévő végzett, kinyitja az ajtót és távozik (mutex feloldása: unlock).
  4. A kint várakozók közül valaki bemehet a mosdóba és újra bezárhatja az ajtót.

Mutex használatánál nem tudjuk, hogy a várakozók közül kinek lesz lehetősége következőként bejutni a mosdóba, azaz nem tudható, hogy melyik szál tudja majd magához ragadni és zárolni a mutexet.

Mutex C++-ban

A C++11-es szabvány óta a C++ támogatja a szálkezelést és a mutexeket a Standard Template Library <mutex> header állományán keresztül. A legfontosabb osztály az std::mutex.

Manuális lezárás és feloldás mutexen keresztül

Az std::mutex típusú objektum lezárható és feloldható a lock() és unlock() metódusokkal. Ez a naív használat viszont veszélyes, mert ha a lock() és unlock() közötti kódban kivétel dobódik el vagy korai kilépés (early return) lesz az adott függvényből, akkor a zárolt mutex feloldása nem történik meg, akkor a zárolt mutex feloldása elmarad, ezért a többi szál akár végleg várakozhat a mutexre.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>

std::mutex mtx;
int counter{ 0 };

void Increase()
{
    mtx.lock(); // Lezárás - a többi szál itt várakozik

    // Kritikus szakasz (csak egy szál futtathatja egyszerre)
    ++counter;

    mtx.unlock(); // Feloldás - egy másik szál zárhatja a mutexet
}

int main()
{
    std::vector<std::thread> threads;
    threads.reserve(10);

    // 10 szálon elindítjuk a növelést
    for (int i = 0; i < 10; ++i) {
        threads.push_back(std::thread{ Increase });
    }

    // Megvárjuk, hogy minden szál futása véget érjen
    for (auto& thread : threads) {
        thread.join();
    }

    std::cout << "Counter's value: " << counter << std::endl; // Mindig 10 lesz.
}

Javasolt és biztonságos mutex használat

Annak érdekében, hogy a lezárt mutex a kritikus szakaszt követően mindenképp feloldásra kerüljön érdemes az std::lock_guard osztályt használni. Ez egy RAII elvű burkoló osztály, aminek a konstruktora lezárja a paraméterként átadott mutexet, destruktora pedig feloldja azt. Így amikor az std::lock_guard típusú objektum kikerül a hatókörből, akkor a lezárt mutex feloldása biztosan megtörténik. Így az std::lock_guard típusú objektum élettartama össze lesz kapcsolva a mutex zárolásával és feloldásával.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void Increase()
{
    // A lock_guard lezárja a mutexet, ha magához tudja ragadni.
    // Ha már zárolt a mutex, akkor várakozik a szál.
    std::lock_guard<std::mutex> lock{ mtx };

    // Kritikus szakasz
    ++counter;

    // A függvény végén lock megszűnik és a destruktora meghívja az mtx.unlock()-ot.
}

Mit érdemes még megismerni std::mutex-hez kapcsolódóan?

  • std::unique_lock típus
  • std::scoped_lock típus
  • try_lock() metódus
  • több mutex egyidejű kezelése
  • deadlock
  • mutex és std::condition_variable