Невозможно быть const-корректным при объединении данных и их блокировки?

Я искал способы объединить часть данных, к которой будут обращаться несколько потоков вместе с блокировкой, обеспеченной для безопасности потока. Я думаю, что дошел до того, что не считаю возможным сделать это, сохраняя при этом правильность констант.

Возьмите следующий класс для примера:

template <typename TType, typename TMutex>
class basic_lockable_type
{

public:
typedef TMutex lock_type;

public:
template <typename... TArgs>
explicit basic_lockable_type(TArgs&&... args)
: TType(std::forward<TArgs...>(args)...) {}

TType& data() { return data_; }
const TType& data() const { return data_; }

void lock() { mutex_.lock(); }
void unlock() { mutex_.unlock(); }

private:
TType           data_;
mutable TMutex  mutex_;

};

typedef basic_lockable_type<std::vector<int>, std::mutex> vector_with_lock;

В этом я пытаюсь объединить данные и блокировки, маркировки mutex_ как mutable, К сожалению, этого недостаточно, потому что когда я его использую, vector_with_lock должен быть отмечен как mutable для того, чтобы операция чтения была выполнена из const функция, которая не совсем корректна (data_ должно быть mutable из const).

void print_values() const
{
std::lock_guard<vector_with_lock> lock(values_);
for(const int val : values_)
{
std::cout << val << std::endl;
}
}

vector_with_lock values_;

Может ли кто-нибудь увидеть вокруг себя такое, что сохраняется правильность констант при объединении данных и блокировки? Кроме того, я сделал какие-то неправильные предположения здесь?

7

Решение

Лично я бы предпочел конструкцию, в которой вам не нужно блокировать вручную, а данные должным образом инкапсулированы таким образом, что вы не сможете получить к ним доступ без предварительной блокировки.

Одним из вариантов является функция друга apply или что-то, что выполняет блокировку, захватывает инкапсулированные данные и передает их функциональному объекту, который запускается с блокировкой, удерживаемой в нем.

//! Applies a function to the contents of a locker_box
/*! Returns the function's result, if any */
template <typename Fun, typename T, typename BasicLockable>
ResultOf<Fun(T&)> apply(Fun&& fun, locker_box<T, BasicLockable>& box) {
std::lock_guard<BasicLockable> lock(box.lock);
return std::forward<Fun>(fun)(box.data);
}
//! Applies a function to the contents of a locker_box
/*! Returns the function's result, if any */
template <typename Fun, typename T, typename BasicLockable>
ResultOf<Fun(T const&)> apply(Fun&& fun, locker_box<T, BasicLockable> const& box) {
std::lock_guard<BasicLockable> lock(box.lock);
return std::forward<Fun>(fun)(box.data);
}

Использование тогда становится:

void print_values() const
{
apply([](std::vector<int> const& the_vector) {
for(const int val : the_vector) {
std::cout << val << std::endl;
}
}, values_);
}

В качестве альтернативы вы можете использовать цикл for на основе диапазона, чтобы правильно ограничить блокировку и извлечь значение как «одну» операцию. Все, что нужно, — это правильный набор итераторов.1:

 for(auto&& the_vector : box.open()) {
// lock is held in this scope
// do our stuff normally
for(const int val : the_vector) {
std::cout << val << std::endl;
}
}

Я думаю, что объяснение в порядке. Общая идея заключается в том, что open() возвращает дескриптор RAII, который получает блокировку на конструкцию и освобождает ее при уничтожении. Цикл for на основе диапазона обеспечит этот временный ресурс до тех пор, пока выполняется этот цикл. Это дает надлежащий объем блокировки.

Этот дескриптор RAII также обеспечивает begin() а также end() итераторы для диапазона с единственным значением. Вот как мы можем получить защищенные данные. Цикл на основе диапазона заботится о том, чтобы выполнить разыменование для нас и связать его с переменной цикла. Поскольку диапазон является единичным, «цикл» фактически всегда будет выполняться ровно один раз.

box не должен предоставлять какой-либо другой способ получить данные, так что он фактически обеспечивает блокированный доступ.

Конечно, можно спрятать ссылку на данные после того, как окно открыто, таким образом, что ссылка становится доступной после закрытия окна. Но это для защиты от Мерфи, а не Макиавелли.

Конструкция выглядит странно, поэтому я бы не стал никого обвинять в том, что он этого не хотел. С одной стороны, я хочу использовать это, потому что семантика идеальна, но с другой стороны, я не хочу, потому что это не то, для чего предназначен диапазон. С захватывающей стороны эта гибридная техника дальнего и дальнего плавания является довольно общей и может быть легко использована для других целей, но я оставлю это на ваше воображение / кошмары;) Используйте по своему усмотрению.


1 Оставлено в качестве упражнения для читателя, но краткий пример такого набора итераторов можно найти в моем собственном реализация locker_box.

6

Другие решения

Что вы понимаете под «const правильной»? Вообще, я думаю, что существует консенсус по логическому константу, что означает, что если мьютекс не является частью логического (или наблюдаемого) состояния вашего объекта, нет ничего плохого в его объявлении mutableи используя его даже в константных функциях.

3

В некотором смысле, является ли мьютекс заблокированным или нет, является частью наблюдаемого состояния объекта — вы можете наблюдать это, например, случайно создав инверсию блокировки.

Это фундаментальная проблема с самоблокирующимися объектами, и я предполагаю, что один из аспектов имеет отношение к константности.

Либо вы можете изменить «заблокированность» объекта с помощью ссылки на const, либо вы не можете осуществлять синхронизированный доступ с помощью ссылки на const. Выберите один, предположительно, первый.

Альтернатива состоит в том, чтобы гарантировать, что объект не может «наблюдаться» вызывающим кодом, когда он находится в заблокированном состоянии, так что заблокированность не является частью наблюдаемого состояния. Но тогда вызывающий абонент не сможет посетить каждый элемент своего vector_with_lock как единая синхронизированная операция. Как только вы вызываете код пользователя с удерживаемой блокировкой, он может написать код, содержащий потенциальную или гарантированную инверсию блокировки, которая «видит», удерживается ли блокировка или нет. Так что для коллекций это не решает проблему.

0
По вопросам рекламы [email protected]