5

I've seen an example for a shared mutex:

class MyData {
    std::vector<double> data_;
    mutable shared_mutex mut_;   // the mutex to protect data_;

public:
    void write() {
        unique_lock<shared_mutex> lk(mut_);
        // ... write to data_ ...
    }

    void read() const {
        shared_lock<shared_mutex> lk(mut_);
        // ... read the data ...
    }
};

Naturally I would have written instead:

public:
    void write() {
        mut_.lock();
        // ... write to data_ ...
        mut_.unlock();
    }

    void read() const {
        mut_.lock_shared();
        // ... read the data ...
        mut_.unlock_shared();
    }
};

Is my way also correct? And is there a difference between what I used and what was used in the example? Also, are there advantages of one over the other? Thank you!

JFMR
  • 23,265
  • 4
  • 52
  • 76
MMM
  • 373
  • 1
  • 4
  • 12
  • Just a side note: If you write `auto lk = unique_lock(mut_);` you can make your code a bit more generic (there is no need for the template argument in that case), and you don't need to worry about of what type `mut_` is, it just needs to meet the requirements `unique_lock` has on the Mutex. – t.niese Jan 19 '20 at 13:08

2 Answers2

12

Is my way also correct?

Consider what would happen if the code between the locking of the mutex and the unlocking throws an exception:

void write() {
    mut_.lock();
    // <-- exception is thrown here
    mut_.unlock();
}

The mutex then remains locked.

are there advantages of one over the other?

Yes, unique_lock<> follows the RAII idiom, and therefore unlocking of the mutex is handled automatically (i.e., by its destructor) in case of an exception:

void write() {
    unique_lock<shared_mutex> lk(mut_);
    // <-- exception is thrown
}

In case of an exception after the creation of the unique_lock<shared_mutex> object – lk – its destructor is called, and it then unlocks the associated mutex if it was locked (remember that std::unique_lock, unlike std::lock_guard, doesn't always have ownership of the lock on the associated mutex – see std::defer_lock and std::unique_lock::unlock()).

To sum up, with lock_guard/unique_lock/shared_lock, no special handling is required from your side in case of exceptions or when leaving the member function from different execution paths.

JFMR
  • 23,265
  • 4
  • 52
  • 76
  • Thanks. One more question if you don't mind: unique_lock lk(mut_); locks from the beginning of the function until its end, right? And: the "lk" is the name of the lock then? – MMM Jan 19 '20 at 11:11
  • 1
    @BenC. `unique_lock lk(mut_);` locks the mutex as soon as it is constructed. In this case, at the very beginning of the function. `lk` is the name of the `unique_lock` object. – JFMR Jan 19 '20 at 11:12
  • So it unlocks the mutex automatically when the function is done? – MMM Jan 19 '20 at 11:14
  • 4
    @BenC. It unlocks the mutex automatically – if it is locked, in your case, it is – at destruction, when `lk` is destroyed. I would suggest you have a look at [`std::lock_guard`](https://en.cppreference.com/w/cpp/thread/lock_guard) as well. `std::unique_lock` is more flexible, but it looks that don't need that flexibility. – JFMR Jan 19 '20 at 11:16
  • Note that since C++17, [`std::scoped_lock` is highly preferred over `std::lock_guard`](https://stackoverflow.com/q/43019598/580083). – Daniel Langr Jan 19 '20 at 11:40
  • @DanielLangr yes, good point, indeed `std::scoped_lock` renders `std::lock_guard` obsolete. Not so, however, with `std::unique_lock` because it implements `unlock()` whereas `std::scoped_lock` doesn't. – JFMR Jan 19 '20 at 11:46
5

Raw mutex is generally avoided in favour of the RAII version unique_lock(), which is safer in two situations:

  • exception
  • premature return

Like raw pointer are generally avoided in favour of RAII version smart pointers: unique_ptr or shared_ptr.

Either case, the RAII version ensure that the mutex (or pointer) is always released when the it goes out of scope.

artm
  • 17,291
  • 6
  • 38
  • 54