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 thier resource following the RAII idiom.  A lock automatically binds its mutex in the constructor and releases it in the destructor. This considerably reduces 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 acces to 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 the 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 is 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 empowers 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)

 

 

 

 

 

 

title page smalltitle page small Go to Leanpub/cpplibrary "What every professional C++ programmer should know about the C++ standard library".   Get your e-book. Support my blog.

 

Tags: lock, mutex

Comments   

0 #11 Chas 2016-11-13 11:37
What's up all, here every one is sharing these kinds of
familiarity, so it's fastidious to read this web site, and I used to
pay a visit this webpage everyday.
Quote
0 #12 Bridgett 2016-11-30 11:45
I do not even know how I ended up here, but I thojght this post was great.
I do not know who you are but definitely you aare going to a famous blogger if you are not already ;
)Cheers!
Quote
0 #13 Desiree 2016-12-01 05:08
Wow! This cann bee one particular of the most useful blogs We've ever
arrive across on this subject. Actually Wonderful. I am also a specialist in this topic therefore I ccan understand your
effort.
Quote
0 #14 mousepickle6.soup.io 2016-12-03 06:39
Does your website have a contact page? I'm having a tough time locating it but,
I'd likle to shoot you aan e-mail. I've gott some ideass for your blog you might be interested in hearing.
Either way, gredat site and I look forward to seeing it
grow ovedr time.
Quote
0 #15 Katherina 2016-12-07 02:32
great points altogether, you simply received a new reader.
Whhat could you suggest in regards to your post that yyou made a few
days in the past? Any sure?
Quote
0 #16 Mike 2016-12-11 00:21
I'm still learning from you, while I'm tryying tto achieve my goals.
I definitely enjoy reading alll that is posted on your site.Keep the aarticles coming.
I lovd it!
Quote
0 #17 goo.gl 2016-12-19 11:06
Hi to every , since I am truly eager of reading this website's post to
be updated regularly. It contains pleasant information.
Quote
0 #18 Golf Thunder 2017-09-27 21:22
Awesome article.
Quote
0 #19 Tennis Thunder 2017-10-05 10:11
Hi there to every body, it's my first pay a quick visit of this blog; this webpage contains amazing
and in fact good data in favor of visitors.
Quote
0 #20 Canada Household 2017-10-09 02:34
Just wish to say your article is as amazing. The clarity to your put up is simply cool and i could think
you're knowledgeable in this subject. Well with your permission allow me to grab your feed to stay
up to date with drawing close post. Thank you a million and please continue the rewarding work.
Quote

Add comment


My Newest E-Books

Latest comments

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 329

All 543224

Currently are 228 guests and no members online