Summary
In this post, I will introduce the correct way to use mutex
in C++, compared to Java.
Conclusion
- Try not to use
std::mutex
directly. - Use
std::unique_lock
,std::lock_guard
, orstd::scoped_lock (since C++17)
to manage locking in a more exception-safe manner. - 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. - Use
std::condition_variable
to control dependency among threads. - 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.
- 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. - You may need multiple global
std::mutex
instances and the same number ofstd::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();
}
}
}