Self-deadlock (önmagával szembeni holtpont) akkor alakulhat ki, amikor ugyanaz a szál kétszer akarja megszerezni ugyanazt a (nem rekurzív) mutexet.

Egyszerű self-deadlock példa

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <mutex>

std::mutex mtx;

void f()
{
    std::lock_guard<std::mutex> lock1{ mtx };
    std::cout << "First lock succeeded.\n";

    // Ugyanaz a szál ismét megpróbálja megszerezni
    // ugyanazt a mutexet.
    std::lock_guard<std::mutex> lock2{ mtx };
    std::cout << "Deadlock happened.\n";
}

int main()
{
    f();
}

f() meghívásakor lock1-en keresztül megszerzésre kerül a mutex (mtx). Ezt követően lock2-n keresztül ugyanezt a mutexet kívánjuk ismét megszerezni, de mivel már a mutexet már ugyanaz a szál birtokolja, a második lockolási kísérlet blokkolódik. Elkezdődik egy várakozás mtx feoldására, viszont az csak akkor történhet meg, amikor lock1 kikerül a hatókörből, azaz f() futása végén.

Persze ilyen jellegű hibát ritkán követünk el, ezért ez teljesen életszerűtlennek tűnik. A probléma különösen veszélyes, ha a lockolás közvetve, több függvényen keresztül történik, mert az egyes függvények önmagukban helyesnek tűnhetnek.

Rejtett self-deadlock

Nézzünk egy példát rejtett(ebb) self-deadlock-ra.

 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
#include <iostream>
#include <mutex>

class Counter
{
private:
    int value = 0;
    std::mutex mtx;

public:
    void Increment()
    {
        std::lock_guard<std::mutex> lock{ mtx };
        ++value;
        Print();
    }

    void Print()
    {
        std::lock_guard<std::mutex> lock{ mtx };
        std::cout << "value = " << value << '\n';
    }
};

int main()
{
    Counter c;
    c.Increment();
}

Két metódusunk van, melyek ugyanazt a mutexet lockolják. Mivel Increment meghívja a Print()-et, a Print()-beli lockolási kísérletnél deadlock alakul ki.

Self-deadlock elkerülése std::recursive_mutex használatával

C++-ban létezik a rekurzív mutex (std::recursive_mutex), mely megengedi, hogy ugyanaz a szál többször is megszerezze ugyanazt a mutexet. Emiatt a fenti példák rekurzív mutex használata esetén nem vezethetnek self-deadlock-hoz.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <iostream>
#include <mutex>

std::recursive_mutex mtx;

void f()
{
    std::lock_guard<std::recursive_mutex> lock1{ mtx };
    std::cout << "First lock\n";
    std::lock_guard<std::recursive_mutex> lock2{ mtx };
    std::cout << "Second lock\n";
}

int main()
{
    f();
}

Rekurzív mutex használata esetén is figyelni kell arra, hogy a mutexet ugyanannyiszor kell lockolni, mint ahányszor feloldjuk (unlock). Szerencsére std::lock_guard használatával garantált, hogy a lock/unlock párok száma rendben lesz, mert a lezárás a konstruktorban valósul meg, a feloldás pedig a destruktorban.

Az std::recursive_mutex használata azonban nem feltétlenül az ideális megoldás a felmerülő tervezési problémára. Gyakran célszerűbb a mutexelés struktúráját átalakítani.

Helper függvények használatakor kialakuló deadlock

Tekintsünk egy olyan példát, amikor egy folyószámla egyenlegét szeretnénk mutex-szel védeni. Van egy BankAccount osztályunk, amelynek mind publikus, mint privát (helper) metódusai mutex-szel védik a balance adattagot.

 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
36
37
#include <iostream>
#include <mutex>

class BankAccount
{
private:
    double balance{ 1000.0 };
    std::mutex mtx;

    void LogBalance()
    {
        std::lock_guard<std::mutex> lock{ mtx };
        std::cout << "Balance: " << balance << '\n';
    }

    void UpdateBalance(const double amount)
    {
        std::lock_guard<std::mutex> lock{ mtx };
        balance += amount;
        LogBalance(); // Ártalmatlannak tűnik
    }

public:
    void Deposit(const double amount)
    {
        std::lock_guard<std::mutex> lock{ mtx };
        std::cout << "Depositing " << amount << '\n';
        UpdateBalance(amount);
    }
};

int main()
{
    BankAccount account;
    account.Deposit(500.0);
    std::cout << "Done.\n";
}

Ez egy tipikus példája a self-deadlock-nak, hiszen minden függvény védi az adattagot, de ezzel előidézik holtpont kialakulását.

Publikus lockol, privát nem lockol

A probléma egyik lehetséges feloldása lehet, ha a publikus metódusok mutex-szel védik az adattagot, míg a privát (helper) metódusok már nem nyújtanak ilyen védelmet, hanem “bíznak” a publikus metódusok által nyújott védelemben.

 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
class BankAccount
{
private:
    double balance{ 1000.0 };
    std::mutex mtx;

    // Előfeltevés: mtx már lockolt
    void PrintBalanceImpl()
    {
        std::cout << "Balance: " << balance << '\n';
    }

public:
    void PrintBalance()
    {
        std::lock_guard<std::mutex> lock{ mtx };
        PrintBalanceImpl();
    }

    void Deposit(double amount)
    {
        std::lock_guard<std::mutex> lock{ mtx };
        balance += amount;
        PrintBalanceImpl();
    }
};

Felhívom a figyelmet arra, hogy a példában PrintBalanceImpl() önmagában nem szálbiztos: meghívója felelőssége, hogy a mutex már lockolva legyen.

Ezt a mintát lockek/unlocked member function pattern-nek is szokás nevezni.