As we know already, data race exists whenever two threads access the same location in memory, at least one access modifies the location, and the two accesses are not ordered by a happens before relationship. Typically, the way to ensure that the latter is enforced for plain shared data is to protect it with a mutex.
A mutex ("mutual exclusion") is a synchronization primitive which may be owned by at most one thread at a time. A thread which wishes to enter a region of code a critical section which performs reads or writes to the protected data first acquires exclusive ownership of the mutex.
If another thread already owns the mutex, it will be blocked until its owner releases it. When one thread releases ownership of a mutex and another thread immediately acquires it, a happens before relationship is created, and a data race on the data guarded by that mutex is prevented.
The std library type that implements this concept is std::mutex, which is found in the header .
The basic operations on a std::mutex are lock and unlock. Lock gives exclusive access; if the mutex is already locked, it blocks until the current owner has unlocked it.
Once work on the protected data is complete, unlock is called, allowing other threads that have called lock on the same mutex to proceed.
#include <iostream>
#include <thread>
#include <mutex>
int counter = 0;
std::mutex counter_mutex;
void increment()
{
for (int i = 0; i < 100000; ++i) {
counter_mutex.lock();
++counter;
counter_mutex.unlock();
}
}
int main()
{
// Declare t1 before code needs to call it
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Final value: " << counter << '\
';
return 0;
}
In the above program, two threads repeatedly perform the same sequence of actions: lock the mutex, increment a shared counter, then unlock it. Since both threads may modify counter only when locked, neither thread has the ability to affect a modification performed by the other.
Since both unlock calls execute happens before the next lock, as required by the memory model, counter is guaranteed to end up at 200000.
This is only safe so long as all control flow which acquired the lock will eventually release the lock again. If an exception is thrown after a lock, or a return statement is executed with a lock obtained, the mutex remains locked eternally.
Any other thread attempting to lock it will then block forever the program has deadlocked.
void unsafe_increment()
{
counter_mutex.lock();
// ... code that might throw
++counter;
counter_mutex.unlock(); // never reached if an exception occurred
}
Because it is so dangerous to leave mutable state unprotected, C++ does not widely support explicit pairs of lock and unlock. The standard approach to protecting variables is by binding the ownership of a lock to the scope of an automatic object, ensuring that if the object falls out of scope (even due to an exception), the lock is automatically released.
The std library type for precisely this is called std::lock_guard. std::lock_guard locks the mutex on construction and unlocks it on destruction. This often results in fewer lines of code, as well as improved security against exceptions.
#include <iostream>
#include <thread>
#include <mutex>
int counter = 0;
std::mutex counter_mutex;
void increment()
{
for (int i = 0; i < 100000; ++i) {
// Hold the mutex for the lifetime of this guard
std::lock_guard<std::mutex> lock(counter_mutex);
++counter;
} // lock is destroyed here and the mutex is unlocked
}
int main()
{
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Final value: " << counter << '\
';
return 0;
}
Each thread initializes a std::lock_guard, which locks counter_mutex in its constructor. At the end of the block, it goes out of scope and its destructor unlocks counter_mutex, even if an exception was thrown within the critical section.
Reading or writing to an object without the required synchronization can result in a data race if another thread performs a conflicting, unsynchronized access, where at least one access is a modification. However, not executing operations in the correct order might lead to violating program invariants even if no data race occurs in a specific run.
Likewise, you must not attempt to lock an already locked non recursive mutex again while holding ownership (this will cause undefined behavior, most often deadlock).
If you need this, use std::recursive_mutex.
This same strategy can be applied to any shared data whatsoever. The common idiom is to create the mutex adjacent to the data that it protects, to acquire exclusive ownership of the mutex, and then carry out an operation which, in isolation, must be atomic.
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <string>
std::vector<std::string> log_messages;
std::mutex log_mutex;
// Bind by reference without allowing modification through it
void add_message(const std::string& msg)
{
std::lock_guard<std::mutex> lock(log_mutex);
log_messages.push_back(msg);
}
void print_log()
{
std::lock_guard<std::mutex> lock(log_mutex);
for (const auto& m : log_messages)
std::cout << m << '\
';
}
int main()
{
std::thread t1(add_message, "Thread one started");
std::thread t2(add_message, "Thread two started");
t1.join();
t2.join();
print_log();
return 0;
}
Concurrency between multiple operations to the same standard container is not automatically managed by standard containers. Reading threads can likely safely read, but writer threads have to be synchronized with other writers to the same object.
Keep the guard and the protected data together. Often, both will be private member variables of a class that you design, and each public member function may acquire the lock on that member variable before calling a service that operates on that data member.
Lock critical sections as short as possible. While a thread holds the lock, other threads requiring access are unable to proceed. Long lock periods lead to diminished concurrency.
Always use RAII locks like
std::lock_guard(or the more flexiblestd::unique_lockorstd::scoped_lockfrom the following article). They have few disadvantages and many advantages over explicitlock()/unlock()pairs.Remember that a mutex only guarantees exclusion, not order beyond what the unlock/lock ordering specifies.
0 comments
No comments yet. Be the first.