Как обойти блокировку мьютекса из-под рекурсивного вызова
Имеется дерево из инстансов некоторых классов A и B (псевдокод):
class A
{
public:
void callB()
{
m.lock();
// ...
// много всякого непотокобезопасного кода
// ...
b.callF();
m.unlock();
}
private:
B b;
std::mutex m;
};
class B
{
public:
void callF()
{
f();
}
private:
std::function<void()> f;
std::mutex m;
}
Вызов B::f() может привести к вызову A::callB() извне. Однако никак нельзя убрать блокировку мьютексом в A::callB(), т.к. это может быть обычный нерекурсивный вызов. Понятно, что использование std::recursive_mutex решает проблему, но хотелось бы обойтись без него.
Пробовал выносить вызов callF() в A::callB() за границу защиты мьютекса:
class A
{
public:
void callB()
{
m.lock();
// ...
// много всякого непотокобезопасного кода
// ...
m.unlock();
b.callF();
}
...
но это приводило к усложнению кода, плюс, поскольку таких вызовов в реальной задаче несколько, к нарушению целостности выполнения операции A::callB(), т.к. несколько тредов начинают менять значения членов A в конкурентном порядке. Что-то типа такого:
class A
{
public:
void call()
{
m.lock();
// ...
val = 10;
m.unlock();
b.callF(); // здесь val то 10, то 20
m.lock();
// ...
val = 20;
m.unlock();
c.callF(); // здесь val то 10, то 20
}
private:
B b;
C c;
int val;
std::mutex m;
};
Какую технику возможно использовать, чтобы, с одной стороны, обойти блокировки при рекурсивном вызове, а с другой - сохранить целостность всей операции?
Ответы (1 шт):
Есть мнение, что решения в обозначенных рамках не существует. Точнее - есть, их много, но каждое из них привнесёт с собой собственные проблемы, необходимость в компромиссах и т.п.
Например, можно пойти по пути использования асинхронности, как то имеет место в том же Qt, и организовать для обратных вызовов (коллбэков) некую очередь. Но в таком случае эту очередь кто-то должен постоянно опрашивать и исполнять содержащиеся в ней коллбэки. Будет ли это некий глобальный пул потоков или в текущем потоке будет организован цикл-обработчик подобных событий, всё это в любом случае ведёт к разрыву рекурсии и добавлению новых сущностей, в которых изначально не было никакой необходимости.
Или, например, можно озадачиться проверкой какого-нибудь атомарного флага, который будет говорить, является ли текущий поток уже защищённым соответствующим мьютексом или нет. В этом случае мьютекс не будет блокироваться рекурсивно, будет всегда известно, что любой вызов unlock() приведёт к полному его освобождению, а не возможно частичному, которое очевидно свойственно рекурсивному мьютексу. Но это решение также привносит и свои пять копеек в набор подводных камней. Придётся плодить эти флаги или таскать ссылки на них в дереве объектов и располагать на каждом уровне в тех местах, где используется каждый из мьютексов.
Или, например, можно организовать передачу коллбэка в качестве аргумента функции, чтобы не разрывать рекурсию. Что-то вроде: запоминаем в аргументе функтор, контекст выполнения возвращается по дереву вызовов на самый верхний уровень, освобождает ранее заблокированный мьютекс, и вот тогда уже вызывает коллбэк. Но и в этом решении проявляется куча проблем, начиная с того, что коллбэков по ходу движения вниз по иерархии вызовов может быть много и заканчивая необходимостью для программиста следить за тем, когда и на каком из уровней следует исполнять тот или иной коллбэк. Всё это однозначно превратится в головную боль.
Чем же в свою очередь плох рекурсивный мьютекс? Как минимум:
- в каждом конкретном контексте вызов unlock() не гарантирует полного освобождения мьютекса;
- мьютекс должен быть заблокирован на максимально короткое время (иначе зачем нам многопоточность?), что при рекурсивной блокировке становится слабо достижимо;
- оверхед из-за проверки на текущий тред всякий раз при каждой блокировке.