Skip to content

luketokheim/lockables

Repository files navigation

Lockables

CI

Lockables are class templates for mutex based concurrency in C++17. Synchronize data between multiple threads using locks.

Quick start

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);
}

Anti-patterns: Do not do this!

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();
// }

References

Package manager

This project uses the Conan C++ package manager for Continuous Integration (CI) and to build Docker images.

Build

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.

Contributing

See the CONTRIBUTING document.

About

Lockables are class templates for mutex based concurrency in C++17.

Resources

License

Stars

Watchers

Forks