Mutex in C++ and Java

Summary

In this post, I will introduce the correct way to use mutex in C++, compared to Java.

Conclusion

  1. Try not to use std::mutex directly.
  2. Use std::unique_lock, std::lock_guard, or std::scoped_lock (since C++17) to manage locking in a more exception-safe manner.
  3. Undefined behaviors will happen if
    a). A mutex is destroyed while still owned by any threads;
    b). A thread terminates while owning a mutex;
    c). std::mutex.lock() is called by a thread that already owns the mutex;
    d). std::mutex.unlock() is called but the mutex is not currently locked by the calling thread.
  4. Use std::condition_variable to control dependency among threads.
  5. In Java, java.util.concurrent.Semaphore is able to achieve both exclusive blocks and dependency control among threads.

Some experience below is helpful for concurrency coding challenges.

  1. Use internal boolean or integer variables to help to manage dependencies among threads; use these variables to make some threads sleeping before one thread uses std::condition_variable to notify them.
  2. You may need multiple global std::mutex instances and the same number of std::condition_variable instances to transfer computation from a thread to another thread.

Details

1. Managment of exclusive blocks

Use std::unique_lock. Examples are shown below.

#include <mutex>
#include <thread>
#include <iostream>
#include <vector>

int main() {
    int counter = 0;
    std::mutex counter_mutex;
    std::vector<std::thread> threads;

    auto worker_task = [&](int id) {
    // acquire mutex when unique_lock is created
        std::unique_lock<std::mutex> lock(counter_mutex);
        ++counter;
        std::cout << id << ", initial counter: " << counter << '\n';
        // gurantee to release lock after leaving the block, including exceptions, goto, return, etc. 
    };

    for (int i = 0; i < 10; ++i) threads.emplace_back(worker_task, i);

    for (auto &thread : threads) thread.join();
}

std::unique_lock is able to ensure that every time only one thread is running codes in the block. Compared to using std::mutex directly, std::unique_lock also ensures that the mutex will unlock when leaving the scope where the std::unique_lock is defined.

2. Dependency among threads

Use std::condition_variable to manage dependency among threads. std::condition_variable use wait(unique_lock) and notify_all() or notify_one() heavily. It talks to std::unique_lock and often use internal variables (boolean or integer) to help determining the execution order. Examples are shown below.

#include <iostream>
#include <thread>
#include <condition_variable>
#include <mutex>
#include <chrono>
#include <queue>
using namespace std;

condition_variable cond_var;
mutex m;

int main() {
    int value = 100;
    bool notified = false;
    thread reporter([&]() {
        unique_lock<mutex> lock(m);
        while (!notified) {
            cond_var.wait(lock);
        }
        cout << "The value is " << value << endl;
    });

    thread assigner([&]() {
        value = 20;
        notified = true;
        cond_var.notify_one();
    });

    reporter.join();
    assigner.join();
    return 0;
}

In the above codes, we use a boolean notified to guarantee that the reporter always works after the assigner. Note that we use a while loop in the reporter since generally, the function is notified to wake up by a call in another thread either to member notify_one or to member notify_all, but certain implementations may produce spurious wake-up calls without any of these functions being called. Therefore, we use a while loop to avoid spurious wake-up.

3. Semaphore makes life easier

Java’s Semaphore contains the functionality of both mutex and condition_variable in C++. See the following coding challenge for example.

import java.util.concurrent.*;
class FooBar {
    private int n;
    Semaphore s1, s2;
    public FooBar(int n) {
        this.n = n;
        s1 = new Semaphore(1);
        s2 = new Semaphore(0);
    }

    public void foo(Runnable printFoo) throws InterruptedException {

        for (int i = 0; i < n; i++) {
            s1.acquire();
            // printFoo.run() outputs "foo"
            printFoo.run();
            // release acts like notifying
            s2.release();
        }
    }

    public void bar(Runnable printBar) throws InterruptedException {
        for (int i = 0; i < n; i++) {
            s2.acquire();
            // printBar.run() outputs "bar"
            printBar.run();
            s1.release();
        }
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *