我一直在研究如何组合一个数据,这些数据将由多个线程访问,同时为线程安全提供锁定。我想我已经达到了这样的程度,即我认为不可能这样做,同时保持正确性。
以下面的课程为例:
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
函数执行读取操作t完全正确(data_
应该是来自const的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_;
任何人都可以看到这一点,以便在组合数据和锁定时保持const正确性?另外,我在这里做了不正确的假设吗?
答案 0 :(得分:6)
就个人而言,我更喜欢一种你不需要手动锁定的设计,并且数据被正确封装,使你无法在不先锁定的情况下实际访问它。
一个选项是拥有一个友元函数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 implementation中找到这样一组迭代器的简短示例。
答案 1 :(得分:3)
你对“const correct”的理解是什么?一般来说,我认为逻辑const有一个共识,这意味着如果互斥体不是对象的逻辑(或可观察)状态的一部分,那么声明它mutable
并使用它是没有错的。甚至在const函数中。
答案 2 :(得分:0)
在某种意义上,互斥锁是否被锁定是对象的可观察状态的一部分 - 您可以通过例如意外创建锁定反转来观察它。
这是自锁对象的一个基本问题,我想它的一个方面确实与const-correctness有关。
要么你可以改变&#34;锁定&#34;通过引用到const的对象,或者你不能通过reference-to-const进行同步访问。选择一个,大概是第一个。
另一种方法是确保不能观察到物体。通过调用代码处于锁定状态时,锁定性不是可观察状态的一部分。但是,调用者无法访问vector_with_lock
中的每个元素作为单个同步操作。只要您在保持锁定的情况下调用用户代码,他们就可以编写包含潜在或保证锁定反转的代码,并且&#34;看到&#34;是否持有锁。因此,对于集合,这并不能解决问题。