HiveBrain v1.2.0
Get Started
← Back to all entries
patterncppMinor

The dumbest (futex-based) mutex

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
themutexbasedfutexdumbest

Problem

After reading Ulrich Drepper's "Futexes are Tricky", I have written the following "dumbest mutex" in C++14 using the Linux futex primitives. This mutex is simpler than Drepper's; I believe it to be correct. The reason it gets to be so simple is the same as the reason I'm calling it the "dumbest mutex": it makes a system call on every unlock, even if the mutex is not being contended and nobody's waiting for the lock.

Therefore this is not a good mutex if you care about performance.

My question is: Leaving aside the performance issue, is this a correct mutex? Or does it, like most hand-written concurrency code, have some subtle bug?
Stylistic comments are also welcome.

#include 
#include 
#include 
#include 

inline int futex_wait(void *addr, int block_if_value_is) {
    return syscall(SYS_futex, addr, block_if_value_is, nullptr, nullptr, 0);
}

inline int futex_wake_one(void *addr) {
    return syscall(SYS_futex, addr, FUTEX_WAKE, 1, nullptr, nullptr, 0);
}

class mutex {
    std::atomic m_state;
    static constexpr int UNLOCKED = 0;
    static constexpr int LOCKED = 1;
public:
    constexpr mutex() noexcept : m_state(UNLOCKED) {}

    bool try_lock() {
        return m_state.exchange(LOCKED) == UNLOCKED;
    }

    void lock() {
        while (m_state.exchange(LOCKED) != UNLOCKED) {
            futex_wait(&m_state, LOCKED);
        }
    }

    void unlock() {
        m_state = UNLOCKED;
        futex_wake_one(&m_state);
    }
};

Solution

is this a correct mutex?

It is not correct. There is a requirement for std::mutex::lock (http://en.cppreference.com/w/cpp/thread/mutex/lock):


If another thread has already locked the mutex, a call to lock will
block execution until the lock is acquired.

A simple test below that uses your mutex consumes 100% CPU that means it is busy-waiting. I think you did it by mistake.

On the contrary if I use std::mutex for the same test consumes 0% CPU in waiting on mt.lock().

mutex mt;
int main() {
    auto f = []() {
        mt.lock();
        mt.unlock();
    };
    mt.lock();
    std::thread t (f);
    std::this_thread::sleep_for(std::chrono::seconds(60));
    mt.unlock();
    t.join();
}


Below is a pidstat report for running a test with your mutex:

$ pidstat 1 -p $(pidof custom_mutex)  
Linux 3.10.0-229.el7.x86_64 (sk71.net.billing.ru)       05/16/2017      _x86_64_        (2 CPU)

09:44:31 AM   UID       PID    %usr %system  %guest    %CPU   CPU  Command
09:44:32 AM  1000      9806   11.00   88.00    0.00   99.00     0  custom_mutex
09:44:33 AM  1000      9806   14.00   86.00    0.00  100.00     0  custom_mutex
09:44:34 AM  1000      9806   13.00   86.00    0.00   99.00     0  custom_mutex
09:44:35 AM  1000      9806   15.00   84.00    0.00   99.00     0  custom_mutex
09:44:36 AM  1000      9806   15.00   83.00    0.00   98.00     0  custom_mutex
09:44:37 AM  1000      9806   12.00   87.00    0.00   99.00     0  custom_mutex
09:44:38 AM  1000      9806   16.00   83.00    0.00   99.00     0  custom_mutex
09:44:39 AM  1000      9806   15.00   85.00    0.00  100.00     0  custom_mutex


And this is a report for a test that uses std::mutex:

$ pidstat 1 -p $(pidof std_mutex)    
Linux 3.10.0-229.el7.x86_64 (sk71.net.billing.ru)       05/16/2017      _x86_64_        (2 CPU)

09:46:45 AM   UID       PID    %usr %system  %guest    %CPU   CPU  Command
09:46:46 AM  1000      9965    0.00    0.00    0.00    0.00     0  std_mutex
09:46:47 AM  1000      9965    0.00    0.00    0.00    0.00     0  std_mutex
09:46:48 AM  1000      9965    0.00    0.00    0.00    0.00     0  std_mutex
09:46:49 AM  1000      9965    0.00    0.00    0.00    0.00     0  std_mutex
09:46:50 AM  1000      9965    0.00    0.00    0.00    0.00     0  std_mutex
09:46:51 AM  1000      9965    0.00    0.00    0.00    0.00     0  std_mutex
09:46:52 AM  1000      9965    0.00    0.00    0.00    0.00     0  std_mutex
09:46:53 AM  1000      9965    0.00    0.00    0.00    0.00     0  std_mutex


update

I believe your mistake is in parameters for syscall. FUTEX_WAKE on my Linux is equal to 1. But take a look at:

inline int futex_wait(void *addr, int block_if_value_is) {
    return syscall(SYS_futex, addr, block_if_value_is, nullptr, nullptr, 0);
}


It passes block_if_value_is to SYS_futex as a parameter for int op. However the value of block_if_value_is in your program is equal to LOCKED and LOCKED is 1 (it means that it is equal to FUTEX_WAKE):

static constexpr int UNLOCKED = 0;
static constexpr int LOCKED = 1;

futex_wait(&m_state, LOCKED);


So you do not wait. You actually ask the kernel to wake up 0 (nullptr) processes. That is why you instead of waiting in kernel on your futex returns:

return syscall(SYS_futex, addr, block_if_value_is, nullptr, nullptr, 0);

Code Snippets

mutex mt;
int main() {
    auto f = []() {
        mt.lock();
        mt.unlock();
    };
    mt.lock();
    std::thread t (f);
    std::this_thread::sleep_for(std::chrono::seconds(60));
    mt.unlock();
    t.join();
}
$ pidstat 1 -p $(pidof custom_mutex)  
Linux 3.10.0-229.el7.x86_64 (sk71.net.billing.ru)       05/16/2017      _x86_64_        (2 CPU)

09:44:31 AM   UID       PID    %usr %system  %guest    %CPU   CPU  Command
09:44:32 AM  1000      9806   11.00   88.00    0.00   99.00     0  custom_mutex
09:44:33 AM  1000      9806   14.00   86.00    0.00  100.00     0  custom_mutex
09:44:34 AM  1000      9806   13.00   86.00    0.00   99.00     0  custom_mutex
09:44:35 AM  1000      9806   15.00   84.00    0.00   99.00     0  custom_mutex
09:44:36 AM  1000      9806   15.00   83.00    0.00   98.00     0  custom_mutex
09:44:37 AM  1000      9806   12.00   87.00    0.00   99.00     0  custom_mutex
09:44:38 AM  1000      9806   16.00   83.00    0.00   99.00     0  custom_mutex
09:44:39 AM  1000      9806   15.00   85.00    0.00  100.00     0  custom_mutex
$ pidstat 1 -p $(pidof std_mutex)    
Linux 3.10.0-229.el7.x86_64 (sk71.net.billing.ru)       05/16/2017      _x86_64_        (2 CPU)

09:46:45 AM   UID       PID    %usr %system  %guest    %CPU   CPU  Command
09:46:46 AM  1000      9965    0.00    0.00    0.00    0.00     0  std_mutex
09:46:47 AM  1000      9965    0.00    0.00    0.00    0.00     0  std_mutex
09:46:48 AM  1000      9965    0.00    0.00    0.00    0.00     0  std_mutex
09:46:49 AM  1000      9965    0.00    0.00    0.00    0.00     0  std_mutex
09:46:50 AM  1000      9965    0.00    0.00    0.00    0.00     0  std_mutex
09:46:51 AM  1000      9965    0.00    0.00    0.00    0.00     0  std_mutex
09:46:52 AM  1000      9965    0.00    0.00    0.00    0.00     0  std_mutex
09:46:53 AM  1000      9965    0.00    0.00    0.00    0.00     0  std_mutex
inline int futex_wait(void *addr, int block_if_value_is) {
    return syscall(SYS_futex, addr, block_if_value_is, nullptr, nullptr, 0);
}
static constexpr int UNLOCKED = 0;
static constexpr int LOCKED = 1;

futex_wait(&m_state, LOCKED);

Context

StackExchange Code Review Q#163367, answer score: 9

Revisions (0)

No revisions yet.