Wednesday 3 May 2017

C++11 Features - Solution for Deadlock

1. Problem Description
On my previous blog, Threading - Deadlock, an example of "BankAccount" shows how deadlock is to happen. Account A is to do a transfer to Account B and at the same time Account B is to do one to Account A. The first transaction is to lock A then lock B and do the transfer. The second transaction is to lock B then lock A and do the transfer. There is a good chance that only the first step happened during the two transactions. The first transaction locks A and the second locks B and both try to get the second locking. But fail to do so and two transaction are locked mutually.
The solutions proposed on Threading - Deadlock still work but C++11 and C++17 just have better solutions.

2. C++11 and C++17 Solutions
The solution proposed on Threading - Deadlock is to lock the mutex in the order to guarantee that only one thread can acquire all the resources at once. The solution of C++11 is std::lock, which is able to lock multiple lockable objects at once - in the scenario of none or all.
std::lock either locks all the lockable objects or locks none of them in case of failure (for instance one of lockable objects isn't called in parity of lock/unlock).

C++11 solutions
std::lock can used either with std::lock_guard or std::unique_lock. Comparing std::lock_guard and std::unique_lock, they have similar functionalities. std::lock_guard can be regarded as RAII wrapper of lockable objects via reference and however std::unique_lock can be regarded as RAII wrapper of lockable objects via pointer and with ownership indicator.

*******************************************************************************
class BankAccount{
public:
    void Pay();
    void Receive();
    void Transfer(BankAccount& rhs, double amount)
private:
    std::mutex m_Lock;
    long long sort_code;
    long long account_no;
    std::string name;
    std::string postcode
};

void BankAccount::Transfer(BankAccount& rhs, double amount)
{
    if (*this == rhs) {
        return;
    }
  
    // std::lock to lock both mutex
    // std::adopt_lock is to tell std::guard to take ownership only
    // rather than lock the object in the ctor
    std::lock(m_Lock, rhs.m_Lock);
    std::lock_guard<std::mutex> lock1(m_Lock, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(rhs.m_Lock, std::adopt_lock);

    // do other stuff
}

void BankAccount::Transfer(BankAccount& rhs, double amount)
{
    if (*this == rhs) {
        return;
    }

    // std::defer_lock to tell std::unique_lock not to lock the object
    // just take the ownership
    // std::lock is to both mutex
    std::unique_lock<std::mutex> lock1(m_Lock, std::defer_lock);
    std::unique_lock<std::mutex> lock2(rhs.m_Lock, std::defer_lock);
    std::lock(m_Lock, rhs.m_Lock);

    // do other stuff
}
*******************************************************************************

Beside std::unique_lock is not copyable but movable but std::lock_guard is neither copyable nor movable. Therefore std::unique_lock takes extra memory, runs slower but is more flexible than std::lock_guard. A typical use of std::lock_gurad as the automatic variables for lock/unlock lockable objects. And std::unique_luck is to take the ownership of lockable objects and transfer the ownership between functions (for instance as a returning object of function).

C++17 solution
A solution to replace the combination of std::lock and std::lock_guard in C++11 is provided in C++17 as std::scoped_lock. It works as a RAII wrapper of std::lock and std::lock_guard in C++11. Very clean and beautiful.

*******************************************************************************
void BankAccount::Transfer(BankAccount& rhs, double amount)
{
    if (*this == rhs) {
        return;
    }
    std::scoped_lock sl(m_Lock, rhs.m_Lock);
    

    // do other stuff
}
*******************************************************************************




No comments:

Post a Comment