Prefer Locks to Mutexes

Contents[Show]

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 flavours in C++11. std::lock_guard for the simple,  and std::unique-lock for the advanced use case.

std::lock_guard

First, the simple use case.

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

 

With so little code mutex m ensures access of the critical section sharedVariable= getVar() is sequential. Sequential means  - in this special 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 if the programmer simply 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();
}

 

That was easy. But what's 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. Exactly at that time point, 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 limit 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 different sequence. /* "locked in different sequence." needs editing*/

 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 exactly  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;

}

 

In case you call the constructor of std::unique_lock with the argument std::defer_lock, the lock will not be locked automatically. It happens in line 14 and 19. The lock operation is performed atomically in line 23 by using the variadic template std::lock. A variadic template is a template which can accept an arbitrary number of arguments. Here, the arguments are locks. std::lock tries to get the 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, while it is holding a resource, a deadlock lurks near.

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 are the cause for 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 by 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 exactly 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 at the same time. (Proofreader Alexey Elymanov)

 

 

 

 

Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, Marko, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, espkk, Louis St-Amour, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Neil Wang, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Tobi Heideman, Daniel Hufschläger, Red Trip, Alexander Schwarz, Tornike Porchxidze, Alessandro Pezzato, Evangelos Denaxas, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Satish Vangipuram, and Michael Dunsky.

 

Thanks in particular to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, and Said Mert Turkal.

 

 

My special thanks to Embarcadero CBUIDER STUDIO FINAL ICONS 1024 Small

 

Seminars

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

Bookable (Online)

German

Standard Seminars (English/German)

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

New

Contact Me

Modernes C++,

RainerGrimmSmall

Tags: lock, mutex

Comments   

0 #21 helloworld 2021-06-15 02:43
in the case of unique_lock, without
```cpp
std::lock(guard1, guard2);
```
also gives the right result?
Quote

My Newest E-Books

Course: Modern C++ Concurrency in Practice

Course: C++ Standard Library including C++14 & C++17

Course: Embedded Programming with Modern C++

Course: Generic Programming (Templates)

Course: C++ Fundamentals for Professionals

Interactive Course: The All-in-One Guide to C++20

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 5866

Yesterday 11310

Week 28150

Month 188824

All 6837516

Currently are 154 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments