6

1つのスレッドだけがコンテナーにアイテムを追加したり、コンテナーからアイテムを削除したりできるように、スレッドセーフなコンテナーを(C ++で)実装する必要があります。私は以前、スレッド間でミューテックスを共有することによってこの種のことを行いました。これにより、コード全体に多くのミューテックスオブジェクトが散らばり、物事が非常に乱雑になり、保守が困難になります。

これを行うためのよりきちんとした、よりオブジェクト指向の方法があるかどうか疑問に思いました。コンテナの周りに次の単純なクラスラッパーを考えました(半疑似C ++コード)

 class LockedList {
    private:
        std::list<MyClass> m_List;

    public:
        MutexObject Mutex;
 };

次のようにロックできるようにします

 LockedList lockableList;     //create instance
 lockableList.Mutex.Lock();    // Lock object

 ... // search and add or remove items

 lockableList.Mutex.Unlock();   // Unlock object

だから私の質問は、これが設計の観点から良いアプローチであるかどうかを尋ねることです。メンバーへのパブリックアクセスを許可することは、デザインの観点からは嫌われていることを私は知っていますが、上記のデザインには重大な欠陥がありますか?もしそうなら、スレッドセーフなコンテナオブジェクトを実装するためのより良い方法はありますか?

デザインとC++全般に関する本をたくさん読んだことがありますが、マルチスレッドプログラミングとマルチスレッドソフトウェアデザインに関する文献は本当に不足しているようです。

上記が問題を解決するための不十分なアプローチである場合、誰かがそれを改善する方法を提案するか、スレッドセーフになるようにクラスを設計するための良い方法を説明するいくつかの情報を私に指摘することができますか?どうもありがとう。

4

6 に答える 6

7

RAII を使用して、例外をより安全にするために、このようなことを行います。

class LockedList {
private:
    std::list<MyClass> m_List;
    MutexObject Mutex;
    friend class LockableListLock;
};

class LockableListLock {
private:
    LockedList& list_;
public:
    LockableListLock(LockedList& list) : list_(list) { list.Mutex.Lock(); }
    ~LockableListLock(){ list.Mutex.Unlock(); }
}

このように使用します

LockableList list;
{
    LockableListLock lock(list); // The list is now locked.

    // do stuff to the list

} // The list is automatically unlocked when lock goes out of scope.

LockableListLock の std::list のインターフェイスの周りにラッパーを追加することで、何かを行う前にクラスを強制的にロックすることもできます。そのため、LockedList クラスを介してリストにアクセスする代わりに、LockableListLock クラスを介してリストにアクセスします。たとえば、std::list::begin() の周りにこのラッパーを作成します。

std::list::iterator LockableListLock::begin() {
    return list_.m_List.begin();
}

そして、このように使用します

LockableList list;
LockableListLock lock(list);
// list.begin();   //This is a compiler error so you can't 
                   //access the list without locking it
lock.begin(); // This gets you the beginning of the list
于 2012-08-01T16:01:36.723 に答える
7

ミューテックスをロックし、スレッドで使用できるオブジェクトを返すリソース所有者を設計したいと思います。スレッドが処理を終了し、オブジェクトの使用を停止すると、リソースはautomaticallyその所有者に返され、ロックが解除されます。

template<typename Resource>
class ResourceOwner
{
      Lock         lock; 
      Resource     resource;

      public:
         ResourceHolder<Resource>  getExclusiveAccess()
         {
              // Let the ResourceHolder lock and unlock the lock
              // So while a thread holds a copy of this object only it
              // can access the resource. Once the thread releases all
              // copies then the lock is released allowing another
              // thread to call getExclusiveAccess().
              //
              // Make it behave like a form of smart pointer
              //    1) So you can pass it around.
              //    2) So all properties of the resource are provided via ->
              //    3) So the lock is automatically released when the thread
              //       releases the object.

              return ResourceHolder<Resource>(lock, resource);
         }
};

リソースホルダー (難しく考えていないので改善可能)

template<typename Resource>
class ResourceHolder<
{
    // Use a shared_ptr to hold the scopped lock
    // When first created will lock the lock. When the shared_ptr
    // destroyes the scopped lock (after all copies are gone)
    // this will unlock the lock thus allowding other to use
    // getExclusiveAccess() on the owner
    std::shared_ptr<scopped_lock>    locker;
    Resource&                        resource;   // local reference on the resource.

    public:
        ResourceHolder(Lock& lock, Resource& r)
            : locker(new scopped_lock(lock))
            , resource(r)
        {}

        // Access to the resource via the -> operator
        // Thus allowing you to use all normal functionality of 
        // the resource.
        Resource* operator->() {return &resource;}
};

ロック可能なリストは次のとおりです。

ResourceOwner<list<int>>  lockedList;

void threadedCode()
{
    ResourceHolder<list<int>>  list = lockedList.getExclusiveAccess();

    list->push_back(1);
}
// When list goes out of scope here. 
// It is destroyed and the the member locker will unlock `lock`
// in its destructor thus allowing the next thread to call getExclusiveAccess()
于 2012-08-01T16:06:47.857 に答える
2

さて、私は他の人がすでに示唆していることをもう少し直接的に述べます:このデザインの少なくとも一部、そしておそらくすべては、おそらくあなたが望むものではありません。少なくとも、RAIIスタイルのロックが必要です。

また、locked(またはあなたがそれと呼んでいるものを)テンプレートにして、コンテナ自体からロックを切り離すことができるようにします。

// C++ like pesudo-code. Not intended to compile as-is.
struct mutex {
    void lock() { /* ... */ }
    void unlock() { /* ... */ }
};

struct lock {
    lock(mutex &m) { m.lock(); }
    ~lock(mutex &m) { m.unlock(); }
};

template <class container>
class locked {
    typedef container::value_type value_type;
    typedef container::reference_type reference_type;
    // ...

    container c;
    mutex m;
public:
    void push_back(reference_type const t) {
        lock l(m);
        c.push_back(t);
    }

    void push_front(reference_type const t) { 
        lock l(m);
        c.push_front(t);
    }

    // etc.
};

これにより、コードの記述がかなり簡単になり、(少なくとも場合によっては)正しい動作が得られます。たとえば、シングルスレッドコードは次のようになります。

std::vector<int> x;

x.push_back(y);

...スレッドセーフなコードは次のようになります。

locked<std::vector<int> > x;

x.push_back(y);

通常の、、、などを指定すると、通常begin()のコンテナと同じように使用できるため、標準のアルゴリズム、イテレータなどで動作します。end()push_frontpush_backlocked<container>

于 2012-08-01T16:22:27.407 に答える
1

このアプローチの問題点は、LockedList がコピー不可になることです。この問題の詳細については、次の質問をご覧ください。

スレッドセーフなコピー可能クラスの設計

私は何年にもわたってさまざまなことを試してきましたが、コンテナ宣言の横に宣言されたミューテックスは、常に最も簡単な方法であることがわかりました(他のメソッドを素朴に実装した後、すべてのバグが修正された後)。

コードにミューテックスを「散らかす」必要はありません。保護するコンテナーの横に宣言されたミューテックスが 1 つだけ必要です。

于 2012-08-01T16:02:12.760 に答える
0

粗粒度のロックが悪い設計上の決定であるとは言い難いです。それについて話すには、コードが存在するシステムについて知る必要があります。ただし、うまくいかないことがわからない場合は、良い出発点です。最初に機能する可能性のある最も単純なことを実行します。

ただし、ロックを解除せずにスコープを設定した場合に失敗する可能性を低くすることで、そのコードを改善できます。

struct ScopedLocker {
  ScopedLocker(MutexObject &mo_) : mo(mo_) { mo.Lock(); }
  ~ScopedLocker() { mo.Unlock(); }

  MutexObject &mo;
};

実装をユーザーから隠すこともできます。

class LockedList {
  private:
    std::list<MyClass> m_List;
    MutexObject Mutex;

  public:
    struct ScopedLocker {
       ScopedLocker(LockedList &ll);
       ~ScopedLocker();
    };
};

次に、ロックされたリストを渡すだけで、の詳細について心配する必要はありませんMutexObject

リストにすべてのロックを内部的に処理させることもできますが、これは場合によっては問題ありません。設計上の問題は反復です。リストが内部的にロックされている場合、このような操作は、いつロックするかをリストのユーザーに決定させるよりもはるかに悪いものです。

void foo(LockedList &list) {
  for (size_t i = 0; i < 100000000; i++) {
    list.push_back(i);
  }
}

一般的に言えば、このような問題があるため、アドバイスを与えるのは難しいトピックです. 多くの場合、オブジェクトをどのように使用するかが問題になります。マルチプロセッサ プログラミングを解決するコードを書き込もうとすると、漏れやすい抽象化がたくさんあります。そのため、人々がニーズを満たすソリューションを構成できるようにするツールキットが増えています。

数は少ないですが、マルチプロセッサ プログラミングについて説明している本があります。すべての新しい C++11 機能が公開されているため、今後数年以内にさらに多くの資料が公開されるはずです。

于 2012-08-01T15:43:45.000 に答える