在组合数据时不可能是恒定正确的,并且它是锁定的?
Impossible to be const-correct when combining data and it's lock?
我一直在寻找将一段将由多个线程访问的数据与为线程安全提供的锁相结合的方法。我想我已经到了这样一个地步,我认为在保持常量正确性的同时不可能做到这一点。
以以下类为例:
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
中的每个元素作为单个同步操作进行访问。一旦您在持有锁的情况下调用用户的代码,他们就可以编写包含潜在或有保证的锁反转的代码,从而"查看"是否持有锁。因此,对于收藏品来说,这并不能解决问题。
- 防止主数据类型C++的隐式转换
- 用于访问容器<T>数据成员的正确 API
- C++ 11 中的锁定是否保证访问数据的新鲜度?
- 锁定的互斥锁是否保护condition_variable和数据?
- 我可以将部分数据用作锁定吗?
- 通知线程是否始终需要在修改期间锁定共享数据
- 试图更新和插入时,数据核心会被锁定
- 锁定免费的C++数据结构,不可能
- 在并发数据结构中,什么级别的锁定粒度是好的
- 在组合数据时不可能是恒定正确的,并且它是锁定的?
- 测试与C 原始数据类型上同步锁定的需求
- 互斥锁 / 什么是被锁定的互斥锁数据
- 我应该在多线程编程中始终锁定全局数据吗?为什么
- 为什么开发人员在多线程代码的读/写过程中锁定字长数据
- 是否可以使用互斥锁来锁定数据结构中的一个元素?
- 使用原子锁定自由的单生产者多消费者数据结构
- 树莓派从MPU6050+BMP180读取数据时锁定
- 在Linux上锁定对单个进程内数据访问的最快方法
- 如何锁定由属于两个不同类的两个线程修改的数据结构
- 是否有必要锁定STL容器以读取其数据?