0

i am looking at this piece of code:

#include <chrono> #include <iostream> #include <map> #include <mutex> #include <shared_mutex> #include <string> #include <thread> bool flag; std::mutex m; void wait_for_flag() { // std::cout << &m << std::endl; // return; std::unique_lock<std::mutex> lk(m); while (!flag) { lk.unlock(); std::cout << "unlocked....." << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::cout << "sleeping....." << std::endl; lk.lock(); std::cout << "locked by " << std::this_thread::get_id() << "....." << std::endl; } } int main(int argc, char const *argv[]) { std::thread t(wait_for_flag); std::thread t2(wait_for_flag); std::thread t3(wait_for_flag); std::thread t4(wait_for_flag); std::thread t5(wait_for_flag); t.join(); t2.join(); t3.join(); t4.join(); t5.join(); return 0; } 

I am new to this, and I thought mutex can only be acquired by one thread. I got two questions:

  1. why there is no deadlock among those threads, e.g. if thread A runs lk.unlock(), then thread B runs lk.lock() and then thread A runs lk.lock().
  2. what does it mean we define a new unique_lock in every thread associating to the same mutex lock (which is called m in here)

Thanks

12
  • Is there any reason you did not initialize flag? Commented Sep 13, 2022 at 8:09
  • lk.unlock(); this unlocks the mutex as soon as you have locked it. Seems pointless. Commented Sep 13, 2022 at 8:10
  • Unique lock is just a helper that locks the underlying mutex for you. If this is already locked by another thread, the current thread will wait until it is unlocked. Commented Sep 13, 2022 at 8:10
  • it seems like not necessary, right? Commented Sep 13, 2022 at 8:10
  • 1
    @Edee the waiting is implemented by the mutex. The lock just calls lock and unlock on the mutex, the same as you could do manually without the lock (but then you run in trouble when there is an exception). Basically what I wrote in the answer Commented Sep 13, 2022 at 8:25

3 Answers 3

3

Because right after acquiring a lock on the mutex each thread calls lk.unlock(); and now other thread can acquire a lock on the mutex. Only if a thread tries to lock an already locked mutex (by a different thread) it has to wait for the mutex to be free. As any thread in your code eventually calls lk.unlock(); there is always a chance for a different thread to get a lock on the mutex and there is no deadlock.

A deadlock would occur for example if you have two mutexes and two threads try to lock them in different order:

 // thread A std::unique_lock<std::mutex> lk1(mutex1); std::unique_lock<std::mutex> lk2(mutex2); // X // thread B std::unique_lock<std::mutex> lk2(mutex2); std::unique_lock<std::mutex> lk1(mutex1); // X 

Here it can happen that thread A locks mutex1, thread B locks mutex2 and then both wait in X for the other thread to release the other mutex, but this will never happen. Its a deadlock.

2.

A lock is merely a slim RAII type. Its only purpose is to call lock on the mutex when created and unlock when destroyed. You can write the same code without the lock, by manually locking / unlocking the mutex, but when there is an exception while a mutex is locked it will never be unlocked.

Sign up to request clarification or add additional context in comments.

7 Comments

So, it mutex m is locked via the unique_lock wrapper in one thread, another thread B tries to lock it as well, thread B will wait and keep silent, right? I know how the multithreading works, I am not familiar with the operation of unique_lock
@Edee if a thread tries to lock a mutex that is locked by a different thread the call will block. Whether this is done via a lock or manually makes little difference
@Edee, Maybe you are confused by the name unique_lock. I never liked it. I was taught that "lock" and "mutex" were synonyms. I was taught that a "lock" was like the lock on a door—a permanent object with two states. But in C++ nomenclature, "lock" and "mutex" are different things. In C++ "lock" is an ephemeral thing that represents the ownership of a mutex by a thread. When you leave your house, you might say, "I locked my front door," but when your wait_for_flag() function executes unique_lock<mutex> lk(m); we say something like, "it puts/takes/creates a lock on m."
@Edee, So, what "unique" means in unique_lock is that only one variable at a time can represent the lock that some thread has on the mutex. That's in contrast to a shared_lock. If your program had created a shared_lock lk(m), it then could make copies of lk, and the lock on m would not be released until all of the copies had been destroyed. But a unique_lock cannot be copied. That's what "unique" is supposed to tell you.
|
1

@SolomonSlow my question is, if we use unique_lock to wrap the mutex in different threads, why there is no deadlock...?

"Deadlock" means that there is some set of threads in which none of the threads can proceed until one of the other members of the set does something. In the simplest possible deadlock, there are just two threads, and there are two mutexes:

  • Thread A has placed a unique_lock on mutex 1, and it is blocked, waiting to place a lock on mutex 2.
  • Thread B has placed a lock on mutex 2, and it is blocked, waiting to place a lock on mutex 1.

Thread A can't do anything until thread B does something first, and thread B can't do anything until thread A does something first. Neither thread will ever be able to do anything again. Deadlock.

You can't have a deadlock without at least two different things (e.g., two different mutexes) that the threads wait for. If there's only one mutex, then whichever thread has it locked, that thread will be able to proceed. It's only a deadlock when no thread is able to proceed.

In your example, each of the five threads settles in to a loop:

  • unlock the mutex,
  • print, sleep, print,
  • lock the mutex,
  • print,
  • go back to the top of the loop.

Whenever one of your threads locks the mutex, there's nothing to stop it from printing and then going back to the top and unlocking the mutex again so that some other thread can run. There's no deadlock.

2 Comments

Thanks for the answer. To touch the bottom of my question, please grant me my nagging. So, in my example, there is only one mutex instance, but there is one lock in each thread and they are different. So what's happening is, if there is one thread acquired the lock, then if there is another thread try to 'unique_lock<mutex> lk(m)', it will busy-waiting for this mutex m, because it is currently acquired by one thread already. What I need to make sure is, there is a busy-waiting process out there.
@Edee, I don't like the name unique_lock, and I don't like that unique_lock has lock() and unlock() methods. I think the name and those methods are drawing your attention away from what is really important, which is the underlying mutex m. I added a new "answer" (search this page for "just an illustration") which, I hope, may help you to see what the unique_lock in your code actually is doing to the mutex.
1

This is not an answer. It's just an illustration. I turned your one example into three different examples that all achieve the same result. I hope it may help you to better understand what unique_lock does.

The first way doesn't use unique_lock at all. It only uses the mutex. This is the old-school way—the way we used to do things before RAII was discovered.

std::mutex m; { ... while (...) { do_work_outside_critical_section(); m.lock(); // explicitly put a "lock" on the mutex. do_work_inside_critical_section(); m.unlock(); // explicitly remove the "lock." } } 

The old-school way is risky because if do_work_inside_critical_section() throws an exception, it will leave the mutex in a locked state, and any thread that tries to lock it again probably will hang forever.


The second way uses unique_lock, which is an embodiment of RAII.

The RAII pattern ensures that there's no way out of this code block that leaves a lock on mutex m. The unique_lock destructor always will be called, no matter what, and the destructor removes the lock.

std::mutex m; { ... while (...) { do_work_outside_critical_section(); std::unique_lock<std::mutex> lk(m); // constructor puts a "lock" on the mutex. do_work_inside_critical_section(); } // destructor implicitly removes the "lock." } 

Notice that in this version, a unique_lock is constructed and destructed every time around the loop. That might sound costly, but it really isn't. unique_lock is meant to be used in this way.


The last way is what you did in your example. It only creates and destroys the unique_lock one time, but then it repeatedly locks and unlocks it within the loop. This works, but it's more code lines than the version above, which makes it a little bit harder to read and understand.

std::mutex m; { ... std::unique_lock<std::mutex> lk(m); // constructor puts a "lock" on the mutex. while (...) { lk.unlock(); // explicitly remove the "lock" from the mutex. do_work_outside_critical_section(); lk.lock(); // explicitly put a "lock" back on the mutex. do_work_inside_critical_section(); } } // destructor implicitly removes the "lock." 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.