Friday 4 November 2016

Step by step multithreading : Chapter 6, Deadlock.

We all had been went through "the definition of Deadlock" many times during our graduation. Lets implement it in our code and do the practical implementation of it in CPP.

I will try to create cyclic dependency. For that, I need to have more then two mutex and one resource, for which I will create cyclic dependency, go through below mentioned very easy example - 

// print messages on to the output console.
#include<iostream>
#include<string>
#include<mutex>
#include<thread>
using namespace std;

class MsgOnConsole
{
    public:
        void ShowMsgFunctionOne(string str, int i)
        {
            lock_guard<mutex> nowLockWithGuard01(m_mutex01);
            lock_guard<mutex> nowLockWithGuard02(m_mutex02);
            cout << "From " << str << " : "<< i<<endl;
        }
        void ShowMsgFunctionTwo(string str, int i)
        {
            lock_guard<mutex> nowLockWithGuard02(m_mutex02);
            lock_guard<mutex> nowLockWithGuard01(m_mutex01);
            cout << "From " << str << " : "<< i<<endl;
        }
    private:
        mutex m_mutex01;
        mutex m_mutex02;
};
void SomeFunction(MsgOnConsole& msg)
{
    for(int index = 0; index > -100 ; index--)
    {
        msg.ShowMsgFunctionOne("From thread  ", index);
    }
}
int main(int argc, char **argv)
{
    MsgOnConsole msg;
    thread t1(SomeFunction, ref(msg));
    for(int index = 0; index < 100 ; index++)
    {
        msg.ShowMsgFunctionTwo("From main  ", index);
    }
    t1.join();// wait for t1 to finish.
    return 0;
}
/*
compilation option - g++ -std=c++11 blog01.cpp
*/

The CPP code is self explanatory, I will explain it but look at below mentioned diagram - 



ShowMsgFunctionOne has already put lock on m_mutex01 and now looking for another lock m_mutex02, which is locked by ShowMsgFunctionTwo and ShowMsgFunctionTwo is looking for other lock m_mutex01 which is locked by ShowMsgFunctionOne.

A clear example of cyclic dependency, and hence we got the deadlock and program will be in hung state, compile it through c++11 compiler and try it.

Well we have reproduced it, now the important question - "how we can fix deadlock?"
An easy (but not always a standard way) is to lock the mutexes in same order from both the functions.

If the order of locking resource are same from every place, we can remove deadlock. It means, if we have 3 lock resources, resource01, resource02 and resource03. Which we have locked in the order - resource02, resource01, resource03. It has to get locked in same order, from every other place, in your code - first lock resource02 then resource01 then resource03, to avoid deadlock. 

Look at the below mentioned (same program with solution) for more detail -

// print messages on to the output console.
#include<iostream>
#include<string>
#include<mutex>
#include<thread>
using namespace std;

class MsgOnConsole
{
    public:
        void ShowMsgFunctionOne(string str, int i)
        {
            lock_guard<mutex> nowLockWithGuard01(m_mutex01);
            lock_guard<mutex> nowLockWithGuard02(m_mutex02);
            cout << "From " << str << " : "<< i<<endl;
        }
        void ShowMsgFunctionTwo(string str, int i)
        {
            lock_guard<mutex> nowLockWithGuard01(m_mutex01);
            lock_guard<mutex> nowLockWithGuard02(m_mutex02);
            cout << "From " << str << " : "<< i<<endl;
        }
    private:
        mutex m_mutex01;
        mutex m_mutex02;
};
void SomeFunction(MsgOnConsole& msg)
{
    for(int index = 0; index > -100 ; index--)
    {
        msg.ShowMsgFunctionOne("From thread  ", index);
    }
}
int main(int argc, char **argv)
{
    MsgOnConsole msg;
    thread t1(SomeFunction, ref(msg));
    for(int index = 0; index < 100 ; index++)
    {
        msg.ShowMsgFunctionTwo("From main  ", index);
    }
    t1.join();// wait for t1 to finish.
    return 0;
}
/*
compilation option - g++ -std=c++11 blog01.cpp
*/

You can see, from both the functions we are locking mutex in same order.

This approach will solve deadlock, but there will be some business requirements, for which you may not be able to lock the mutex always in same order. What we can do for such cases? C++ provide standard functions to avoid deadlocks. std::lock() and std::adopt_lock.

look at below mentioned program for more details -
#include<iostream>
#include<string>
#include<mutex>
#include<thread>
using namespace std;

class MsgOnConsole
{
    public:
        void ShowMsgFunctionOne(string str, int i)
        {
            std::lock(m_mutex01,m_mutex02);

            // saying m_mutex is already locked now just adapt that lock 
            lock_guard<mutex> nowLockWithGuard(m_mutex01, std::adopt_lock); 
            
            lock_guard<mutex> nowLockWithGuard02(m_mutex02, std::adopt_lock);
            cout << "From " << str << " : "<< i<<endl;
        }
        void ShowMsgFunctionTwo(string str, int i)
        {
            std::lock(m_mutex01,m_mutex02);

            // saying m_mutex is already locked now just adapt that lock 
            lock_guard<mutex> nowLockWithGuard(m_mutex01, std::adopt_lock); 
            
            lock_guard<mutex> nowLockWithGuard02(m_mutex02, std::adopt_lock);
            cout << "From " << str << " : "<< i<<endl;
        }
    private:
        mutex m_mutex01;
        mutex m_mutex02;
};
void SomeFunction(MsgOnConsole& msg)
{
    for(int index = 0; index > -100 ; index--)
    {
        msg.ShowMsgFunctionOne("From thread  ", index);
    }
}
int main(int argc, char **argv)
{
    MsgOnConsole msg;
    thread t1(SomeFunction, ref(msg));
    for(int index = 0; index < 100 ; index++)
    {
        msg.ShowMsgFunctionTwo("From main  ", index);
    }
    t1.join();// wait for t1 to finish.
    return 0;
}
/*
compilation option - g++ -std=c++11 blog01.cpp
*/

std::lock(m_mutex01,m_mutex02), will have logic to call unlock() if we get deadlock, so it will release the resource to avoid the deadlock. The function signature of std::lock is mentioned below -
template< class Lockable1, class Lockable2, class... LockableN >
void lock( Lockable1& lock1, Lockable2& lock2, LockableN&... lockn );

Apart from the things we had discussed above, there are few points -
  • Instead of putting more then one lock from same scope, try to put lock one by one (one lock per scope), as soon as lock_guard goes out of scope, it will unlock the mutex, like mentioned in below mentioned program - 
    void ShowMsgFunctionOne(string str, int i)
    {
        {
            lock_guard<mutex> nowLockWithGuard01(m_mutex01);
        }
        {
            lock_guard<mutex> nowLockWithGuard02(m_mutex02);
        }
        cout << "From " << str << " : "<< i<<endl;
    }
    void ShowMsgFunctionTwo(string str, int i)
    {
        {
            lock_guard<mutex> nowLockWithGuard02(m_mutex02);
        }
        {
            lock_guard<mutex> nowLockWithGuard01(m_mutex01);
        }
        cout << "From " << str << " : "<< i<<endl;
    }
    
  • If you have already put the lock, then avoid calling any function just after lock, because called function may also have to lock some resource and it will go to nested locking. That may also end up with deadlock in worst cases.

Thanks for reading this, please write questions or some more useful information related to this topic on comment section. To learn more about threading, see the full learning index here, or join multithreading learning page on FB or on G++.

No comments:

Post a Comment