deadlockResolved

Prefer Locks to Mutexes

If the previous post showed something, it’s that you should use mutexes with great care. That’s why you should wrap them in a lock.

Locks

Locks take care of their resource following the RAII idiom.  A lock automatically binds its mutex in the constructor and releases it in the destructor. This considerably reduces the risk of a deadlock because the runtime takes care of the mutex.

Locks are available in two flavors in C++11. std::lock_guard for the simple,  and std::unique-lock for the advanced use case.

std::lock_guard

First is the simple use case.

mutex m;
m.lock();
sharedVariable= getVar();
m.unlock();

 

With so little code, mutex m ensures access to the critical section sharedVariable= getVar() is sequential. Sequential means  – in this particular case –  that each thread gains access to the critical section in order. The code is simple but prone to deadlocks. Deadlock appears if the critical section throws an exception or the programmer forgets to unlock the mutex. With std::lock_guard we can do this more elegant:

{
  std::mutex m,
  std::lock_guard<std::mutex> lockGuard(m);
  sharedVariable= getVar();
}

 

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

Be part of my mentoring programs:

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (starts March 2024)
  • Do you want to stay informed: Subscribe.

     

    That was easy. But what about the opening and closing brackets? The lifetime of std::lock_guard is limited by the brackets (http://en.cppreference.com/w/cpp/language/scope#Block_scope). That means its lifetime ends when it leaves the critical section. At that time, the destructor of std::lock_guard is called, and – I guess you know it – the mutex is released. It happens automatically, and, in addition, it happens if getVar() in sharedVariable = getVar() throws an exception. Of course, function body scope or loop scope also limits the lifetime of an object.

    std::unique_lock

    std::unique_lock is mightier but more expansive than its small brother std::lock_guard.

    A std::unique_lock enables youin addition to std::lock_guard

    • create it without an associated mutex
    • create it without a locked associated mutex
    • explicitly and repeatedly set or release the lock of the associated mutex
    • move the mutex
    • try to lock the mutex
    • delayed lock the associated mutex

    But why is it necessary? Remember the deadlock from the post Risks of mutexes? The reason for the deadlock was the mutexes were locked in a different sequence.

     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
    // deadlock.cpp
    
    #include <iostream>
    #include <chrono>
    #include <mutex>
    #include <thread>
    
    struct CriticalData{
      std::mutex mut;
    };
    
    void deadLock(CriticalData& a, CriticalData& b){
    
      a.mut.lock();
      std::cout << "get the first mutex" << std::endl;
      std::this_thread::sleep_for(std::chrono::milliseconds(1));
      b.mut.lock();
      std::cout << "get the second mutex" << std::endl;
      // do something with a and b
      a.mut.unlock();
      b.mut.unlock();
      
    }
    
    int main(){
    
      CriticalData c1;
      CriticalData c2;
    
      std::thread t1([&]{deadLock(c1,c2);});
      std::thread t2([&]{deadLock(c2,c1);});
    
      t1.join();
      t2.join();
    
    }
    

     

    The solution is easy. The function deadlock has to lock their mutex in an atomic fashion. That’s precisely what happens in the following example.

     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
    38
    39
    40
    41
    42
    // deadlockResolved.cpp
    
    #include <iostream>
    #include <chrono>
    #include <mutex>
    #include <thread>
    
    struct CriticalData{
      std::mutex mut;
    };
    
    void deadLock(CriticalData& a, CriticalData& b){
    
      std::unique_lock<std::mutex>guard1(a.mut,std::defer_lock);
      std::cout << "Thread: " << std::this_thread::get_id() << " first mutex" <<  std::endl;
    
      std::this_thread::sleep_for(std::chrono::milliseconds(1));
    
      std::unique_lock<std::mutex>guard2(b.mut,std::defer_lock);
      std::cout << "    Thread: " << std::this_thread::get_id() << " second mutex" <<  std::endl;
    
      std::cout << "        Thread: " << std::this_thread::get_id() << " get both mutex" << std::endl;
      std::lock(guard1,guard2);
      // do something with a and b
    }
    
    int main(){
    
      std::cout << std::endl;
    
      CriticalData c1;
      CriticalData c2;
    
      std::thread t1([&]{deadLock(c1,c2);});
      std::thread t2([&]{deadLock(c2,c1);});
    
      t1.join();
      t2.join();
    
      std::cout << std::endl;
    
    }
    

     

    If you call the constructor of std::unique_lock with the argument std::defer_lock, the lock will not be locked automatically. It happens in lines 14 and 19. The lock operation is performed atomically in line 23 using the variadic template std::lock. A variadic template is a template that can accept an arbitrary number of arguments. Here, the arguments are locks. std::lock tries to get all locks in an atomic step. So, he fails or gets all of them.

    In this example, std::unique_lock takes care of the lifetime of the resources, std::lock locks the associated mutex. But you can do it the other way around. In the first step, you lock the mutexes; in the second std::unique_lock takes care of the lifetime of resources. Here is a sketch of the second approach.

     

    std::lock(a.mut, b.mut);
    std::lock_guard<std::mutex> guard1(a.mut, std::adopt_lock);
    std::lock_guard<std::mutex> guard2(b.mut, std::adopt_lock);
    

     

    Now, all is fine. The program runs without deadlock.

    deadlockResolved

    A side note: Special deadlocks

    It’s an illusion that only a mutex can produce a deadlock. Each time a thread has to wait for a resource, a deadlock lurks near while it is holding a resource.

    Even a thread is a resource.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // blockJoin.cpp
    
    #include <iostream>
    #include <mutex>
    #include <thread>
    
    std::mutex coutMutex;
    
    int main(){
    
      std::thread t([]{
        std::cout << "Still waiting ..." << std::endl;
        std::lock_guard<std::mutex> lockGuard(coutMutex);
        std::cout << std::this_thread::get_id() << std::endl;
        }
      );
    
      {
        std::lock_guard<std::mutex> lockGuard(coutMutex);
        std::cout << std::this_thread::get_id() << std::endl;
        t.join();
      }
    
    }
    

     

    The program immediately stands still.

    blockJoin

    What’s happening? The lock of output stream std::cout and the waiting of the main thread for its child t cause the deadlock. By observing the output, you can easily see in which order the statements will be performed.

    In the first step, the main thread executes lines 19 – 21. It waits in line 21 using the call t.join() until its child t is done with its work package. The main thread is waiting while it locks the output stream. But that’s precisely the resource the child is waiting for. Two ways to solve this deadlock come to mind.

     

    • The main thread locks the output stream std::cout after the call t.join().

    {
      t.join();
      std::lock_guard<std::mutex> lockGuard(coutMutex);
      std::cout << std::this_thread::get_id() << std::endl;
    }
    
    • The main thread releases its lock by an additional scope. This is done before the t.join() call.

    {
      {
        std::lock_guard<std::mutex> lockGuard(coutMutex);
        std::cout << std::this_thread::get_id() << std::endl;
      }
      t.join();
    }
    

     

    What’s next?

    In the next post, I’ll talk about reader-writer locks. Reader-writer locks empower you since C++14 to distinguish between reading and writing threads. So, the contention on the shared variable will be mitigated because an arbitrary number of reading threads can access the shared variable simultaneously. (Proofreader Alexey Elymanov)

     

     

     

     

    Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, moon, Philipp Lenk, Hobsbawm, and Charles-Jianye Chen.

    Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.

    My special thanks to Embarcadero
    My special thanks to PVS-Studio
    My special thanks to Tipi.build 
    My special thanks to Take Up Code
    My special thanks to SHAVEDYAKS

    Seminars

    I’m happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.

    Standard Seminars (English/German)

    Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.

    • C++ – The Core Language
    • C++ – The Standard Library
    • C++ – Compact
    • C++11 and C++14
    • Concurrency with Modern C++
    • Design Pattern and Architectural Pattern with C++
    • Embedded Programming with Modern C++
    • Generic Programming (Templates) with C++
    • Clean Code with Modern C++
    • C++20

    Online Seminars (German)

    Contact Me

    Modernes C++ Mentoring,

     

     

    2 replies
    1. Ig
      Ig says:

      Hi Rainer,
      Great blog! This is an extremely valuable learning resource.
      Q: in the code deadlockResolved.cpp you use std::unique_lock. Later it says “But you can do it the other way around” and you demonstrate it by using std::lock_guard.
      Is it because for this particular order you have to use that template (along with std::adopt_lock)? or you could just go along again with std::unique_lock?

      Thanks!

      Reply

    Leave a Reply

    Want to join the discussion?
    Feel free to contribute!

    Leave a Reply

    Your email address will not be published. Required fields are marked *