7

スレッドセーフのためにプロビジョニングされたロックと一緒に、複数のスレッドによってアクセスされるデータを組み合わせる方法を検討してきました。私は、const-correctnessを維持しながらこれを行うことができないと思うところまで来ていると思います。

たとえば、次のクラスを見てください。

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マークする必要があるためです ( const からである必要があります)。mutableconstdata_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-correctness が維持されるように、誰かがこれを見ることができますか? また、ここで間違った仮定をしましたか?

4

3 に答える 3

6

個人的には、手動でロックする必要がなく、最初にロックしないと実際にアクセスできないようにデータが適切にカプセル化されている設計を好みます。

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 ハンドルは、値が 1 つ含まれる範囲の反復子も提供begin()します。end()これが、保護されたデータを取得する方法です。範囲ベースのループは、逆参照を行い、それをループ変数にバインドします。範囲はシングルトンであるため、「ループ」は実際には常に正確に 1 回実行されます。

box、実際にインターロック アクセスを強制するため、データを取得する他の方法を提供すべきではありません。

もちろん、ボックスが開いたら、ボックスを閉じた後に参照が利用できるように、データへの参照を隠すことができます。しかし、これはマキャヴェリではなく、マーフィーから身を守るためのものです。

構造は奇妙に見えるので、それを望んでいないことを誰かのせいにするつもりはありません。セマンティクスが完璧なので、これを使用したいのですが、一方で、これは範囲ベースの目的ではないため、使用したくありません。グリップ ハンドでは、この範囲と RAII のハイブリッド手法はかなり一般的であり、他の目的で簡単に悪用される可能性がありますが、それはあなたの想像力/悪夢に任せます ;) 使用はあなた自身の判断で行ってください。


1読者の演習として残しておきますが、このような一連の反復子の短い例は、私自身のlocker_box 実装で見つけることができます。

于 2012-11-20T11:04:54.460 に答える
4

「const correct」で何を理解していますか?一般に、論理 const にはコンセンサスがあると思います。つまり、mutex がオブジェクトの論理 (または観察可能な) 状態の一部でない場合、それを宣言しmutable、const 関数でも使用しても問題はありません。

于 2012-11-20T10:57:06.693 に答える
0

ある意味では、ミューテックスがロックされているかどうかは、オブジェクトの監視可能な状態の一部です。たとえば、誤ってロック反転を作成することで監視できます。

これは、セルフロック オブジェクトの基本的な問題であり、その 1 つの側面は const の正確性に関連していると思います。

constへの参照を介してオブジェクトの「ロック状態」を変更するか、constへの参照を介して同期アクセスを作成できません。1 つ、おそらく最初のものを選択します。

別の方法としては、ロックされた状態が監視可能な状態の一部にならないように、ロックされた状態にある間、呼び出し元のコードによってオブジェクトが「監視」されないようにすることです。ただし、呼び出し元がvector_with_lock単一の同期操作として各要素にアクセスする方法はありません。ロックが保持されているユーザーのコードを呼び出すとすぐに、ロックが保持されているかどうかを「確認」する、潜在的または保証されたロック反転を含むコードを書くことができます。したがって、コレクションの場合、これは問題を解決しません。

于 2012-11-20T11:05:01.380 に答える