在组合数据时不可能是恒定正确的,并且它是锁定的?

Impossible to be const-correct when combining data and it's lock?

本文关键字:锁定 数据 组合 不可能      更新时间:2023-10-16

我一直在寻找将一段将由多个线程访问的数据与为线程安全提供的锁相结合的方法。我想我已经到了这样一个地步,我认为在保持常量正确性的同时不可能做到这一点。

以以下类为例:

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)。

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_;

有人能看到这一点吗?这样在组合数据和锁的同时保持常量的正确性?此外,我在这里有没有做出任何错误的假设?

就我个人而言,我更喜欢这样一种设计,即不必手动锁定,并且数据被正确封装,如果不首先锁定,就无法实际访问数据。

一种选择是拥有一个友元函数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不应该提供任何其他方式来获取数据,这样它实际上就强制执行了互锁访问。

当然,一旦盒子打开,就可以收起对数据的引用,以使引用在盒子关闭后可用。但这是为了防范墨菲,而不是马基雅维利。

这个构造看起来很奇怪,所以我不会责怪任何人不想要它。一方面,我想使用它是因为语义是完美的,但另一方面我不想使用它,因为这不是基于范围的。从扣人心弦的角度来看,这一系列RAII混合技术相当通用,很容易被滥用于其他目的,但我将把它留给你的想象/噩梦;)自行使用。


1留给读者练习,但在我自己的locker_box实现中可以找到这样一组迭代器的一个简短示例。

您对"const correct"的理解是什么?一般来说,我认为逻辑const是一致的,这意味着如果互斥对象不是对象的逻辑(或可观察)状态的一部分,那么将其声明为mutable,甚至在const函数中使用它也没有错。

从某种意义上说,互斥是否被锁定是对象可观察状态的一部分——例如,您可以通过意外创建锁定反转来观察它。

这是自锁对象的一个基本问题,我想它的一个方面确实与常量正确性有关。

您可以通过对const的引用来更改对象的"锁定性",也可以通过对const的引用进行同步访问。选一个,大概是第一个。

另一种选择是确保对象在锁定状态下不会被调用代码"观察",这样锁定性就不是可观察状态的一部分。但是,调用方无法将其vector_with_lock中的每个元素作为单个同步操作进行访问。一旦您在持有锁的情况下调用用户的代码,他们就可以编写包含潜在或有保证的锁反转的代码,从而"查看"是否持有锁。因此,对于收藏品来说,这并不能解决问题。