【问题标题】:Impossible to be const-correct when combining data and it's lock?组合数据和锁定时不可能是正确的?
【发布时间】:2012-11-20 10:51:16
【问题描述】:

我一直在寻找方法来组合一条数据,这些数据将由多个线程访问,以及为线程安全而提供的锁。我想我已经到了一个地步,我认为不可能在保持 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 函数执行读取操作(@987654327 @ 应该是来自 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 正确性?另外,我在这里做了什么不正确的假设吗?

【问题讨论】:

  • lockunlock 设为常量? (另外,它不应该是std::lock_guard&lt;vector_with_lock&gt;吗?如果你不打算使用它,你为什么要制作一个新的可锁定的?)
  • @R.MartinhoFernandes 当然,标记这些 const 将允许不可变的 vector_with_lock 实例调用 lockunlock,是吗?
  • @R.MartinhoFernandes 如果您提供的答案解释了将 const 限定符添加到 lockunlock 我可以接受这个答案
  • 我发布了一个答案,建议采用更封装的设计。

标签: c++ multithreading c++11 thread-safety mutable


【解决方案1】:

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

一种选择是拥有一个友元函数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 中找到这样一组迭代器的简短示例。

【讨论】:

  • O_o 那for 是相当辱骂。
  • 我刚刚注意到我的实现中有一个错误:S Ooops。孩子们,不要在家里使用该代码。
  • 还是有问题吗?如果有,是什么原因?
【解决方案2】:

您对“const 正确”的理解是什么?一般来说,我认为逻辑 const 存在共识,这意味着如果互斥锁不是对象的逻辑(或可观察)状态的一部分,那么声明它 mutable 并使用它没有任何问题const 函数。

【讨论】:

    【解决方案3】:

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

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

    您可以通过对常量的引用来更改对象的“锁定性”,或者您不能通过对常量的引用进行同步访问。选择一个,大概是第一个。

    另一种方法是确保对象在处于锁定状态时不能被调用代码“观察”,因此锁定不是可观察状态的一部分。但是,调用者无法将vector_with_lock 中的每个元素作为单个同步操作来访问。只要您在持有锁的情况下调用用户的代码,他们就可以编写包含潜在或保证的锁定反转的代码,以“查看”是否持有锁。所以对于集合来说,这并不能解决问题。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-06-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多