Functional std::lock, for complex cases

struct A{
  std::mutex lock;
  unsigned int value;
  B* b {nullptr};
}
struct B{
  std::mutex lock;
  unsigned int value;
  A* a {nullptr};
}

struct DataA{
  A a;  
  void swap_A_B_values();
} data_a;
struct DataB{
  B b;  
  void swap_B_A_values();
} data_b;

We want to swap A::value, with B::value. Consider that in A and B, all values accessed under lock. To swap them, we need to lock both A and B. Looks like a job for std::lock ?..

In swap_A_B_values() , we first need to lock A, then B.
In swap_B_A_values() , we first need to lock B, then A.

If we simply lock them from different threads, we’ll get dead-lock.

We need use std::lock here, but we can’t, because std::lock require both Lockables, beforehand. And to get second Lockable, we first must lock first. So…

You need lock_functional!

void DataA::swap_A_B_values() {
    auto l = lock_functional(
        [&](){ return &a.lock; },
        [&](){ return (a.b ? nullptr : &a.b->lock); }
    );
  if (!a.b) return;
  std::swap(a.value, a.b->value);
}

lock_functional accept closures as parameters. Closures must return pointer to Lockable, or null; if null - we stop trying (successfully locked Lockables will remain in locked state).

lock_functional do series of try_locks in user defined order. And return tuple of std::unqiue_locks. If one of the closures return null, std::unqiue_lock associated with this Lockable, and all next, will not own locks.

lock_functional a little bit unsafe, because it may occur in half-locked state, when one of the closures return nullptr. But sometimes, that half-locked state may be enough.

lock_all_functional return std::optional< std::tuple< std::unique_lock > > . It always have all locks in locked state, when std::optional have value.

void DataA::swap_B_A_values() {
    auto l = lock_all_functional(
        [&](){ return &b.lock; },
        [&](){ return (b.a ? nullptr : &b.a->lock); }
    );
  if (!l) return;
  std::swap(b.value, b.a->value);
}

Source code

Comments