简体   繁体   中英

unique_lock same mutex in different thread

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, eg 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

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.

@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 (eg, 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.

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."

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM