Lockables are class templates for mutex based concurrency in C++17. Synchronize data between multiple threads using locks.
Guarded<T>
is a class template that stores
a mutex together with the value it guards.
#include <lockables/guarded.hpp>
int main()
{
lockables::Guarded<int> value{100};
{
// The guard is a pointer like object that owns a lock on value.
auto guard = value.with_exclusive();
// Writer lock until guard goes out of scope.
*guard += 10;
}
int copy = 0;
{
// Reader lock.
const auto guard = value.with_shared();
// Reader lock.
copy = *guard;
}
assert(copy == 110);
}
The Guarded<T>
class methods return a
pointer like object that owns a lock on the guarded value.
#include <lockables/guarded.hpp>
#include <algorithm>
#include <numeric>
#include <vector>
int main()
{
lockables::Guarded<std::vector<int>> value{1, 2, 3, 4, 5};
// The guard allows for multiple operations in one locked scope.
{
auto guard = value.with_exclusive();
// sum = value[0] + ... + value[n - 1]
const int sum = std::reduce(guard->begin(), guard->end());
// value[i] = value[i] + sum(value)
std::transform(guard->begin(), guard->end(), guard->begin(),
[sum](int x) { return x + sum; });
assert(sum == 15);
assert((*guard == std::vector<int>{16, 17, 18, 19, 20}));
}
}
Use the with_exclusive
function for multiple Guarded<T>
values with deadlock avoidance.
#include <lockables/guarded.hpp>
#include <numeric>
#include <vector>
int main()
{
lockables::Guarded<int> value1{10};
lockables::Guarded<std::vector<int>> value2{1, 2, 3, 4, 5};
const int result = lockables::with_exclusive(
[](int& x, std::vector<int>& y) {
// sum = (y[0] + ... + y[n - 1]) * x
const int sum = std::reduce(y.begin(), y.end()) * x;
// y[i] += sum
for (auto& item : y) {
item += sum;
}
return sum;
},
value1, value2);
assert(result == 150);
}
Problem: Data race by keeping an unguarded pointer.
Solution: The user must not keep a pointer or reference to the guarded value outside the locked scope.
lockables::Guarded<int> value;
int* unguarded_pointer{};
{
auto guard = value.with_exclusive();
// No! User must not keep a pointer or reference outside the guarded
// scope.
unguarded_pointer = &(*guard);
}
// No! Data race if another thread is accessing value.
// *unguarded_pointer = -10;
// No! User must not keep a reference to the guarded value.
int& unguarded_reference =
lockables::with_exclusive([](int& x) -> int& { return x; }, value);
// No! Data race if another thread is accessing value.
// unguarded_reference = -20;
Problem: Deadlock with recursive guards.
Solution: A calling thread must not own the mutex prior to calling any of the locking functions.
lockables::Guarded<int> value;
{
auto guard = value.with_exclusive();
// No! Deadlock since this thread already owns a lock on value.
// auto recursive_reader = value.with_shared();
// No! Deadlock again.
// auto recursive_writer = value.with_exclusive();
// No! Deadlock again.
// lockables::with_exclusive([](int& x) {}, value);
}
Problem: Deadlock with multiple guards.
Solution: To lock multiple values, use the with_exclusive
function which
avoids deadlock.
lockables::Guarded<int> value1;
lockables::Guarded<int> value2;
// No! Deadlock possible if another thread locks value1 and value2 in different
// order.
// {
// auto guard2 = value2.with_exclusive();
// auto guard1 = value1.with_exclusive();
// }
-
- CP.20: Use RAII, never plain lock()/unlock()
- CP.22: Never call unknown code while holding a lock (e.g., a callback)
- CP.50: Define a mutex together with the data it guards. Use synchronized_value where possible
-
P2559R1: Plan for Concurrency Technical Specification Version 2
-
P0290R4: apply() for synchronized_value
This project uses the Conan C++ package manager for Continuous Integration (CI) and to build Docker images.
The library is header only with no dependencies except the standard library. Use conan to build unit tests.
conan build . --build=missing -o developer_mode=True
Run tests.
cd build/Release
ctest -C Release
See the BUILDING document for vanilla CMake usage and other build options.
See the CONTRIBUTING document.