Search code examples
c++multithreadingmutexdeadlock

std::shared_mutex cannot get read lock when there is not a write lock


#include "shared_mutex"
#include "thread"
#include "iostream"
#include "string"

using namespace std::chrono_literals;

struct debug_mutex : public std::shared_mutex {
    void lock() noexcept {
        std::string m = "Try To lock, u_count: " + std::to_string(unique_lock_c.load()) + ", s_count" +
                        std::to_string(shared_lock_c.load()) + "\n";
        std::cout << m << std::endl;
        std::shared_mutex::lock();
        unique_lock_c.fetch_add(1);
        std::string m1 = "lock, u_count: " + std::to_string(unique_lock_c.load()) + ", s_count" +
                         std::to_string(shared_lock_c.load()) + "\n";
        std::cout << m1 << std::endl;
    }

    void unlock() noexcept {
        std::string m = "Try To unlock, u_count: " + std::to_string(unique_lock_c.load()) + ", s_count" +
                        std::to_string(shared_lock_c.load()) + "\n";
        std::cout << m << std::endl;
        std::shared_mutex::unlock();
        unique_lock_c.fetch_add(-1);
        std::string m1 = "unlock, u_count: " + std::to_string(unique_lock_c.load()) + ", s_count" +
                         std::to_string(shared_lock_c.load()) + "\n";
        std::cout << m1 << std::endl;
    }

    void lock_shared() noexcept {
        std::string m = "Try To lock_shared, u_count: " + std::to_string(unique_lock_c.load()) + ", s_count" +
                        std::to_string(shared_lock_c.load()) + "\n";
        std::cout << m << std::endl;
        std::shared_mutex::lock_shared();
        shared_lock_c.fetch_add(1);
        std::string m1 = "lock_shared, u_count: " + std::to_string(unique_lock_c.load()) + ", s_count" +
                         std::to_string(shared_lock_c.load()) + "\n";
        std::cout << m1 << std::endl;
    }

    void unlock_shared() noexcept {
        std::string m = "Try To unlock_shared, u_count: " + std::to_string(unique_lock_c.load()) + ", s_count" +
                        std::to_string(shared_lock_c.load()) + "\n";
        std::cout << m << std::endl;
        std::shared_mutex::unlock_shared();
        shared_lock_c.fetch_add(-1);
        std::string m1 = "unlock_shared, u_count: " + std::to_string(unique_lock_c.load()) + ", s_count" +
                         std::to_string(shared_lock_c.load()) + "\n";
        std::cout << m1 << std::endl;
    }

    std::atomic_uint64_t shared_lock_c{0};
    std::atomic_uint64_t unique_lock_c{0};
} mutex;

int main(int argc, char **argv) {
    std::thread t{[]() {
        std::this_thread::sleep_for(10ns);
        while (true) {
            mutex.lock_shared();
            mutex.lock_shared();
            mutex.lock_shared();
            mutex.lock_shared();
            mutex.lock_shared();
            mutex.lock_shared();
            std::this_thread::sleep_for(5ns);
            mutex.unlock_shared();
            mutex.unlock_shared();
            mutex.unlock_shared();
            mutex.unlock_shared();
            mutex.unlock_shared();
            mutex.unlock_shared();
            std::cout << "read lock once" << std::endl;
        }
    }};
    std::thread t1{[]() {
        std::this_thread::sleep_for(5ms);
        while (true) {
            mutex.lock();
            std::this_thread::sleep_for(5ms);
            mutex.unlock();
            std::cout << "write lock once" << std::endl;
        }
    }};
    t.join();
    t1.join();
}

This program is a simple example with logging. (Please ignore things like destructors, just for logging purposes.)

It should run continuously, with read and write locks not interfering with each other, but it gets stuck when it shouldn't.

The log shows that when it gets stuck, it didn't actually acquire the write lock, but the read lock also couldn't be acquired.

Try To lock_shared, u_count: 0, s_count4
lock_shared, u_count: 0, s_count5
Try To lock, u_count: 0, s_count5
Try To lock_shared, u_count: 0, s_count5

Why is this happening? My goal is to have this program run persistently until I manually stop it.


Solution

  • You cannot call lock_shared multiple times on the same thread. mutex.lock_shared(); mutex.lock_shared(); is undefined behaviour. You would need something analogous to std::recursive_mutex but for shared_mutex to do that.

    This UB is probably the cause of the deadlock