组合数据和锁定时不可能是const-correct?

Gra*_*eme 7 c++ multithreading mutable thread-safety c++11

我一直在研究如何组合一个数据,这些数据将由多个线程访问,同时为线程安全提供锁定.我想我已经达到了这样一个程度,即我认为不可能在保持常态的同时做到这一点.

以下面的类为例:

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;
Run Code Online (Sandbox Code Playgroud)

在这里我尝试将数据和锁定结合起来,标记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_;
Run Code Online (Sandbox Code Playgroud)

任何人都可以看到这个,以便在组合数据和锁定时保持const正确性?另外,我在这里做了不正确的假设吗?

R. *_*des 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);
}
Run Code Online (Sandbox Code Playgroud)

用法然后变成:

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

或者,您可以滥用基于范围的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;
    }
 }
Run Code Online (Sandbox Code Playgroud)

我认为解释是有道理的.一般的想法是open()返回一个RAII句柄,它获取构造上的锁并在销毁时释放它.只要该循环执行,基于范围的for循环将确保此临时生命.这给出了适当的锁定范围.

RAII句柄还为具有单个包含值的范围提供begin()end()迭代器.这就是我们如何获取受保护的数据.基于范围的循环负责为我们进行解除引用并将其绑定到循环变量.由于范围是单例,"循环"实际上总是只运行一次.

box不应提供任何其他方式获得数据,因此,它实际上强制互锁访问.

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

该构造看起来很奇怪,所以我不会责怪任何人不想要它.一方面我想使用它,因为语义是完美的,但另一方面我不想,因为这不是基于范围的.在握紧的手上,这种范围-RAII混合技术相当通用,可以很容易地被滥用于其他目的,但我会把它留给你的想象/噩梦;)由你自行决定使用.


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