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_lock
s in user defined order. And return tuple of std::unqiue_lock
s. 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);
}
Comments