関数 - c++17 新機能




スコープ付きロックを戻すにはどうすればいいですか? (2)

たとえば、口座残高の集合を考えてみましょう。 そして、いくつかの異なる口座の残高を確認してから、いくつかの異なる口座の残高を調整する必要がある複雑な機能があります。 操作は、コレクションの他のユーザーに対してアトミックである必要があります。 この種の原子性を提供することを主な仕事とするコレクションクラスがあります。 「正しい」方法は何ですか?

boost :: mutexメンバを持つクラスがあります。 問題は、ミューテックスを保持しながら、呼び出し側がクラスへの一連の呼び出しを実行する必要があることです。 しかし、クラス外のコードにミューテックスを自由に使えるようにしたくはありません。

私がやりたいことはこのようなものです(擬似コード):

class MyClass
{
 private:
  boost::mutex mLock;
 public:
  boost::scoped_lock& ScopedLock(return ScopedLock(mLock));
}

そうすれば、呼び出し側はこれを行うことができます。

MyClass f;
if(foo)
{
 boost::scoped_lock lock(f->GetScopedLock());
 f->LockedFunc1();
 f->LockedFunc2();
}

その考えは、 LockedFunc1LockedFunc2がロックを保持した状態で呼び出されるということです。 lockのデストラクタはf->mLock lockを解除します。

基本的な質問が2つあります。

1)どうすればいいですか。

2)これは賢明ですか?

注:これは、同じ名前の質問とはまったく異なります。boost :: scoped_lockを返します。


これどうやってするの?

代替案1

1つの方法はboost::scoped_lockを持つ型を作ることboost::scoped_lock

class t_scope_lock {
public:
  t_scope_lock(MyClass& myClass);
  ...
private:
  boost::scoped_lock d_lock;
};

MyClassがこのタイプのミューテックスへのアクセスを許可するため。 このクラスがMyClassために特別に書かれているのであれば、それを内部クラスMyClass::t_scoped_lockとして追加するだけMyClass::t_scoped_lock

代替案2

もう1つの方法は、(カスタム)スコープロックのコンストラクタに変換可能なスコープロックで使用するための中間型を作成することです。 それからタイプは彼らが合うと思うように選ぶことができる。 多くの人はカスタムスコープロックを好まないかもしれませんが、それはあなたが望むようにそして適切な程度の制御でアクセスを簡単に指定することを可能にするでしょう。

代替案3

時にはそれはMyClassための抽象化層を追加する方が良いです。 クラスが複雑な場合、これは良い解決策とは言えません。なぜなら、次のようなたくさんのバリアントを提供する必要があるからです。

{
 boost::scoped_lock lock(f->GetScopedLock());
 f->LockedFunc1();
 f->LockedFunc2();
}

代替案4

時にはあなたは別のロックを使用することができます(例えば内部と外部)。

代替案5

#4と同様に、再帰的ロックまたは再書き込みロックを使用できる場合があります。

代替案6

ロックされたラッパー型を使用して、その型のインターフェースの一部へのアクセスを選択的に許可することができます。

class MyClassLockedMutator : StackOnly {
public:
    MyClassLockedMutator(MyClass& myClass);
// ...
    void LockedFunc1() { this->myClass.LockedFunc1(); }
    void LockedFunc2() { this->myClass.LockedFunc2(); }
private:
    MyClass& myClass;
    boost::scoped_lock d_lock; // << locks myClass
};

MyClass f;
MyClassLockedMutator a(f);

a.LockedFunc1();
a.LockedFunc2();

これは賢明ですか?

私はあなたのプログラムの正確な制約が何であるか(それ故、複数の選択肢がある)わからないことを覚えておいてください。

代替案#1、#2、#3、および#6は、(実質的に)パフォーマンスのオーバーヘッドがなく、多くの場合、追加の複雑さはごくわずかです。 しかしながら、それらはクライアントにとって構文的に騒々しいです。 IMOは、構文ノイズを最小限に抑えるよりも、コンパイラが(必要に応じて)チェックできる正確さを強制することのほうが重要です。

代替案#4と#5は、追加のオーバーヘッド/競合またはロック/同時エラーとバグを導入するための良い方法です。 場合によっては、検討に値する単純な置換です。

正確さ、パフォーマンス、そして/または他の制限が重要であるとき、たとえそれがいくらかの構文上のノイズまたは抽象化層を犠牲にしても、それらの複雑さを抽象化またはカプセル化することは完全に意味があると思います。 たとえ私がプログラム全体を書いて保守していたとしても、壊れた変更を導入するのは簡単すぎるので、これを行います。 私にとっては、これはより複雑な可視性のケースであり、正しく使用すれば完全に賢明です。

いくつかの例

mainまでスクロールします - このサンプルは、いくつかのアプローチをまとめたものであるため、かなり混乱しています。

#include <iostream>
#include <boost/thread.hpp>

class MyClass;

class MyClassOperatorBase {
public:
    /* >> public interface */
    bool bazzie(bool foo);
protected:
    MyClassOperatorBase(MyClass& myClass) : d_myClass(myClass) {
    }

    virtual ~MyClassOperatorBase() {
    }

    operator boost::mutex & ();

    MyClass& getMyClass() {
        return this->d_myClass;
    }

    const MyClass& getMyClass() const {
        return this->d_myClass;
    }

protected:
    /* >> required overrides */
    virtual bool imp_bazzie(bool foo) = 0;
private:
    MyClass& d_myClass;
private:
    /* >> prohibited */
    MyClassOperatorBase(const MyClassOperatorBase&);
    MyClassOperatorBase& operator=(const MyClassOperatorBase&);
};

class MyClass {
public:
    MyClass() : mLock() {
    }

    virtual ~MyClass() {
    }

    void LockedFunc1() {
        std::cout << "hello ";
    }

    void LockedFunc2() {
        std::cout << "world\n";
    }

    bool bizzle(bool foo) {
        boost::mutex::scoped_lock lock(this->mLock);

        return this->imp_bizzle(foo);
    }

protected:
    virtual bool imp_bizzle(bool foo) {
        /* would be pure virtual if we did not need to create it for other tests. */
        return foo;
    }

private:
    class t_scope_lock {
    public:
        t_scope_lock(MyClass& myClass) : d_lock(myClass.mLock) {
        }

    private:
        boost::mutex::scoped_lock d_lock;
    };
protected:
    friend class MyClassOperatorBase;
private:
    boost::mutex mLock;
};

MyClassOperatorBase::operator boost::mutex & () {
    return this->getMyClass().mLock;
}

bool MyClassOperatorBase::bazzie(bool foo) {
    MyClass::t_scope_lock lock(this->getMyClass());

    return this->imp_bazzie(foo);
}

class TheirClassOperator : public MyClassOperatorBase {
public:
    TheirClassOperator(MyClass& myClass) : MyClassOperatorBase(myClass) {
    }

    virtual ~TheirClassOperator() {
    }

    bool baz(bool foo) {
        boost::mutex::scoped_lock lock(*this);

        return this->work(foo);
    }

    boost::mutex& evilClientMove() {
        return *this;
    }

protected:
    virtual bool imp_bazzie(bool foo) {
        return this->work(foo);
    }

private:
    bool work(bool foo) {
        MyClass& m(this->getMyClass());

        m.LockedFunc1();
        m.LockedFunc2();
        return foo;
    }
};

class TheirClass : public MyClass {
public:
    TheirClass() : MyClass() {
    }

    virtual ~TheirClass() {
    }

protected:
    virtual bool imp_bizzle(bool foo) {
        std::cout << "hallo, welt!\n";
        return foo;
    }
};

namespace {
/* attempt to restrict the lock's visibility to MyClassOperatorBase types. no virtual required: */
void ExampleA() {
    MyClass my;
    TheirClassOperator their(my);

    their.baz(true);

// boost::mutex::scoped_lock lock(my); << error inaccessible
// boost::mutex::scoped_lock lock(my.mLock); << error inaccessible
// boost::mutex::scoped_lock lock(their); << error inaccessible

    boost::mutex::scoped_lock lock(their.evilClientMove());
}

/* restrict the lock's visibility to MyClassOperatorBase and call through a virtual: */
void ExampleB() {
    MyClass my;
    TheirClassOperator their(my);

    their.bazzie(true);
}

/* if they derive from my class, then life is simple: */
void ExampleC() {
    TheirClass their;

    their.bizzle(true);
}
}

int main(int argc, const char* argv[]) {
    ExampleA();
    ExampleB();
    ExampleC();
    return 0;
}

これが私が現在計画している方法です。 返すことができるScopedLockクラスを作成します。 それを使用するには、クラスはboost::mutexを持ち、そのmutexで構築されたScopedLockを返さなければならない。 呼び出し側はこの関数を使用して自分自身のScopedLockを構築し、呼び出し側のScopedLockはクラスメンバ関数によって作成されたロックを継承します。

ScopedLockは、それを取得するために呼び出したメンバ関数を持つクラスメンバの寿命を超えることはできないため、ポインタは安全です。 そして、あなたは(クラスのロジックによって)唯一のロック解除があることを保証されています。

私が目にする唯一の本当の問題は意図的な虐待でしょう。 たとえば、誰かが自分のScopedLockから新しいScopedLockを作成した場合、もう一方のScopedLock(おそらくより長い寿命を持つ)にロックを継承させることになります。 (それをすると痛くなります。そうしないでください。)

class ScopedLock {
private:
    boost::mutex *mMutex;   // parent object has greater scope, so guaranteed valid
    mutable bool mValid;

    ScopedLock();           // no implementation

public:
    ScopedLock(boost::mutex &mutex) : mMutex(&mutex), mValid(true) {
        mMutex->lock();
    }

    ~ScopedLock() {
        if(mValid) mMutex->unlock();
    }

    ScopedLock(const ScopedLock &sl) {
        mMutex=sl.mMutex;
        if(sl.mValid)
        {
                mValid=true;
                sl.mValid=false;
        }
        else mValid=false;
    }

    ScopedLock &operator=(const ScopedLock &sl)
    { // we inherit any lock the other class member had
        if(mValid) mMutex->unlock();
        mMutex=sl.mMutex;
        if(sl.mValid) {
            mValid=true;
            sl.mValid=false;
        }
    }
};

それはまだ私にはちょっと間違っているように感じます。 Boostの最大のポイントは、あなたがしなければならない可能性が最も高いすべてのものに対して、きれいなインターフェースを提供することだと私は思いました。 これは私にとって非常に日常的なようです。 そしてそれをするためのきれいな方法がないという事実は私を怖がらせます。

更新 :これを行うための "正しい" Boost方法は、ロックホルダーオブジェクトにshared_ptrを使用することです。 最後のポインタが破棄されるとオブジェクトは消え、ロックが解除されます。





boost-thread