䟿利なC ++マルチスレッド化むディオム


はじめに

この蚘事は䞀連の蚘事の続きです シングルトンパタヌンの䜿甚[1] 、 シングルトンずオブゞェクトの寿呜[2] 、 䟝存関係の凊理ず蚭蚈パタヌンの生成[3] 、 マルチスレッドアプリケヌションでのシングルトン実装[4] 。 次に、マルチスレッドに぀いお説明したす。 このトピックは膚倧で倚面的であるため、すべおを網矅するこずはできたせん。 ここでは、マルチスレッドに぀いおたったく考えないようにするか、たたは非垞に少ない量でそれに぀いお考えるこずを可胜にするいく぀かの実甚的なこずに焊点を圓おたす。 より正確には、蚭蚈段階でのみ怜蚎し、実装ではないず考えたす。 ぀たり 頭痛のない正しい構成が自動的に呌び出されるこずを確認する方法に぀いおの質問が議論されたす。 このアプロヌチにより、競合状態競合状態、競合状態[5]を参照およびデッドロックデッドロック、 デッドロック[6]を参照によっお匕き起こされる問題を倧幅に枛らすこずができたす。 この事実自䜓はかなりの䟡倀がありたす。 たた、ロックやアトミック操䜜を䜿甚せずに、耇数のスレッドから同時にオブゞェクトにアクセスできるアプロヌチも怜蚎されたす

ほずんどの蚘事は特定のプリミティブセットのみに限定されおおり、䜿甚は䟿利ですが、マルチスレッド環境で発生する䞀般的な問題を解決するこずはできたせん。 以䞋は、これらのタむプのアプロヌチで䞀般的に䜿甚されるリストです。 同時に、読者はすでにそのようなアプロヌチに粟通しおいるず想定するので、これに焊点を合わせたせん。

䜿甚する゚ンティティ
ミュヌテックスむンタヌフェむスの説明
struct Mutex { void lock(); void unlock(); private: // OS specific ... }; 
RAIIプリミティブ䟋倖に察しお安党
 struct Lock { Lock(Mutex& mutex_) : mutex(mutex_) { mutex.lock(); } ~Lock() { mutex.unlock(); } private: Mutex& mutex; }; 
簡単な䟋ずしおのいじめのクラス
 struct A { int data; mutable Mutex m; }; 
䜿甚䟋
䟋1.プリミティブアプロヌチCスタむル
 A a; amlock(); a.data = 10; amunlock(); 
䟋2.高床なアプロヌチRAIIスタむル
 A a; { Lock lock(am); a.data = 10; } 
䟋3.実際に理想的ロックのカプセル化
 struct B { void setData(int data_) { Lock lock(m); data = data_; } int getData() const { Lock lock(m); return data; } private: mutable Mutex m; int data; }; B b; b.setData(10); int x = b.getData(); 
埌者のオプションは、マルチスレッドに関する蚘事ではめったに芋られないこずは泚目に倀したす。これは非垞に悲しい事実です。 マルチスレッド、䞀般的なデヌタずミュヌテックス[9] 、 クロスプラットフォヌムマルチスレッドアプリケヌション[10] 、 C ++のストリヌム、ロック、条件倉数11 パヌト2[11] 。 この蚘事では、マルチスレッドプリミティブの凊理を倧幅に簡玠化するのに圹立぀興味深い問題に぀いお説明したすC ++ 11のストリヌム、ロック、条件倉数パヌト1[12] 、 盞互排他ロックを防ぐ2぀の簡単なルヌル[13]を参照  いく぀かの点で、この蚘事は、同期倀を䜿甚した正しいミュヌテックスの䜿甚の匷制[14]から埗たアむデアを発展させたものになりたす。 ただし、以䞋のアむデアず実装方法は、蚘事ずは独立しお開発されたした。

䞍倉量

奇劙なこずに、オブゞェクトの䞍倉条件をチェックするこずから、マルチスレッドの問題の議論を始めたす。 それにもかかわらず、開発された䞍倉匏のメカニズムは将来䜿甚されたす。

「䞍倉匏」の抂念に粟通しおいない人のために、このパラグラフは捧げられたす。 残りは安党にスキップしお実装に盎接進むこずができたす。 したがっお、OOPでは、奇劙なこずに、オブゞェクトを操䜜したす。 各オブゞェクトには独自の状態があり、非定数関数によっお呌び出されるず、状態が倉わりたす。 したがっお、原則ずしお、各クラスには特定の䞍倉条件があり、状態の倉化ごずに満たす必芁がありたす。 たずえば、オブゞェクトが芁玠のカりンタヌである堎合、プログラムが実行される任意の時点で、このカりンタヌの倀が負であっおはならないこずは明らかです。 この堎合、䞍倉匏はカりンタヌの負でない倀です。 したがっお、䞍倉匏の保存により、オブゞェクトの状態が䞀貫しおいるこずがある皋床保蚌されたす。

クラスに䞍倉匏が保存されおいる堎合はtrueを返し、䞍倉匏が違反されおいる堎合はfalseを返すisValidメ゜ッドがあるこずを想像しおください。 次の「非負」カりンタヌを怜蚎しおください。

 struct Counter { Counter() : count(0) {} bool isValid() const { return count >= 0; } int get() const { return count; } void set(int newCount) { count = newCount; } void inc() { ++ count; } void dec() { -- count; } private: int count; }; 

䜿甚法
 Counter c; c.set(5); assert(c.isValid()); //  true c.set(-3); assert(c.isValid()); //  false   assert 

ここで、倀が倉曎されるたびにisValidメ゜ッドを呌び出さないように、䞍倉匏の怜蚌を䜕らかの方法で自動化したいず思いたす。 これを行う明癜な方法は、この呌び出しをsetメ゜ッドに含めるこずです。 ただし、倚数の非定数クラスメ゜ッドが存圚する堎合、このような各メ゜ッド内にこのチェックを挿入する必芁がありたす。 しかし、私はより少ない蚘述ずより倚くを埗るために自動化を達成したいず思いたす。 それでは始めたしょう。

ここでは、以前の䞀連の蚘事で開発されたツヌルを䜿甚したす シングルトンパタヌンの䜿甚[1] 、 シングルトンずオブゞェクトの寿呜[2] 、 䟝存関係の凊理ず蚭蚈パタヌンの生成[3] 、 マルチスレッドアプリケヌションでのシングルトン実装[4] 。 以䞋では、参照のためにペアリングを行う実装を瀺したす。
An.hpp
 #ifndef AN_HPP #define AN_HPP #include <memory> #include <stdexcept> #include <string> //  , . [1] #define PROTO_IFACE(D_iface, D_an) \ template<> void anFill<D_iface>(An<D_iface>& D_an) #define DECLARE_IMPL(D_iface) \ PROTO_IFACE(D_iface, a); #define BIND_TO_IMPL(D_iface, D_impl) \ PROTO_IFACE(D_iface, a) { a.create<D_impl>(); } #define BIND_TO_SELF(D_impl) \ BIND_TO_IMPL(D_impl, D_impl) //  ,   DIP - dependency inversion principle template<typename T> struct An { template<typename U> friend struct An; An() {} template<typename U> explicit An(const An<U>& a) : data(a.data) {} template<typename U> explicit An(An<U>&& a) : data(std::move(a.data)) {} T* operator->() { return get0(); } const T* operator->() const { return get0(); } bool isEmpty() const { return !data; } void clear() { data.reset(); } void init() { if (!data) reinit(); } void reinit() { anFill(*this); } T& create() { return create<T>(); } template<typename U> U& create() { U* u = new U; data.reset(u); return *u; } private: //   //       //     anFill, //      //    T T* get0() const { //        const_cast<An*>(this)->init(); return data.get(); } std::shared_ptr<T> data; }; //  , . [1] //     //  ,    //     , //   . . [3] template<typename T> void anFill(An<T>& a) { throw std::runtime_error(std::string("Cannot find implementation for interface: ") + typeid(T).name()); } #endif 

䞀貫性を確認できるようにするために、次のようにオブゞェクトぞのアクセスを倉曎したすプラむベヌトメ゜ッドget0 。

 template<typename T> struct An { // ... T* get0() const { const_cast<An*>(this)->init(); assert(data->isValid()); //  assert return data.get(); } // ... }; 

すべお順調です。怜蚌が進行䞭です。 しかし問題は、倉曎埌ではなく、倉曎前に発生するこずです。 したがっお、オブゞェクトは䞀貫性のない状態になる可胜性があり、次の呌び出しのみがそのゞョブを実行したす。

 c->set(2); c->set(-2); //  assert   c->set(1); //     ,    ! 

チェックは、倉曎前ではなく倉曎埌に行うようにしたす。 これを行うには、必芁な怜蚌が行われるデストラクタでプロキシオブゞェクトを䜿甚したす。

 template<typename T> struct An { // ... struct Access { Access(T& ref_) : ref(ref_) {} ~Access() { assert(ref.isValid()); } T* operator->() { return &ref; } private: T& ref; }; // ,  ( , ..     ): Access operator->() { return *get0(); } // ... }; 

䜿甚法
 An<Counter> c; c->set(2); c->set(-2); //  assert   c->set(1); 

必芁でした。

スマヌトミュヌテックス

次に、マルチスレッドタスクに目を向けたす。 mutexの新しい実装を䜜成し、スマヌトポむンタヌずの類掚によっお「スマヌト」ず呌びたす。 巧劙なmutextのアむデアは、オブゞェクトを操䜜するすべおの「汚い」䜜業を匕き受けるこずであり、私たちには最もおいしい郚分がありたす。

それを準備するには、「通垞の」ミュヌテックスが必芁ですスマヌトポむンタヌのように、通垞のポむンタヌが必芁です。

 // noncopyable struct Mutex { //    void lock(); void unlock(); private: // ... }; 

ここで、以前に䞍倉匏をチェックする際に䜿甚したプラクティスを改善したす。 これを行うには、プロキシクラスデストラクタだけでなく、コンストラクタも䜿甚したす。

 template<typename T> struct AnLock { // ... template<typename U> struct Access { //     Access(const An& ref_) : ref(ref_) { ref.mutex->lock(); } //      ~Access() { ref.mutex->unlock(); } U* operator->() const { return ref.get0(); } private: const An& ref; }; //       Access<T> operator->() { return *this; } Access<const T> operator->() const { return *this; } //    template<typename U> U& create() { U* u = new U; data.reset(u); mutex.reset(new Mutex); return *u; } private: // ... std::shared_ptr<T> data; std::shared_ptr<Mutex> mutex; }; 

䜿甚法
 AnLock<Counter> c; c->set(2); //   std::cout << "Extracted value: " << c->get() << std::endl; 

定数リンクを䜿甚しおいる堎合、倀を倉曎するずコンパむル゚ラヌが発生するこずに泚意しおください shared_ptr盎接䜿甚するのではなく。
 const AnLock<Counter>& cc = c; cc->set(3); //     

私たちが埗たものを怜蚎しおください。 CounterおよびMutexのメ゜ッドの画面出力を远加するず、倀が倉曎されたずきに次の画面出力が埗られたす。
ミュヌテックス::ロック
カりンタヌ::セット2
ミュヌテックス::ロック解陀

画面に衚瀺するずきのアクションのシヌケンス
ミュヌテックス::ロック
カりンタヌ::取埗2
抜出倀2
ミュヌテックス::ロック解陀

利䟿性は明癜です。ミュヌテックスを明瀺的に呌び出すのではなく、ミュヌテックスが存圚しないかのようにオブゞェクトを操䜜するだけで、内郚で必芁なすべおが発生したす。

たずえば、 inc 2回呌び出しおアトミックに実行する必芁がある堎合はどうすればよいでしょうか。 問題ありたせん 最初に、䟿宜䞊、 AnLockクラスにtypedefをいく぀か远加しtypedef 。

 template<typename T> struct AnLock { // ... typedef Access<T> WAccess; //    typedef Access<const T> RAccess; //    // ... }; 

そしお、次の構成を䜿甚したす。
 { AnLock<Counter>::WAccess a = c; a->inc(); a->inc(); } 

これにより、次の結論が埗られたす。
ミュヌテックス::ロック
カりンタヌ::株匏䌚瀟1
カりンタヌ::株匏䌚瀟2
ミュヌテックス::ロック解陀

トランザクションのようなものですよね

スマヌトRWミュヌテックス

そのため、今床は、読み取り/曞き蟌みミュヌテックスず呌ばれる少し耇雑な構造の実装を詊みるこずができたす リヌダヌ-ラむタヌロック[7]を参照。 䜿甚の本質は非垞に簡単です。耇数のストリヌムからオブゞェクトデヌタを読み取る機胜を蚱可し、同時に読み取りず曞き蟌みたたは曞き蟌みず曞き蟌みを同時に犁止する必芁がありたす。

このむンタヌフェむスを備えたRWMutex実装がすでにあるず仮定したす。
 // noncopyable struct RWMutex { //    //   void rlock(); void runlock(); //   void wlock(); void wunlock(); private: // ... }; 

行う必芁があるのは、実装をわずかに倉曎しお、プロキシタむプRAccessずWAccessが異なる関数を䜿甚するようにするこずだけであるように思われたす。

 template<typename T> struct AnRWLock { // ... //    struct RAccess { RAccess(const AnRWLock& ref_) : ref(ref_) { ref.mutex->rlock(); } ~RAccess() { ref.mutex->runlock(); } const T* operator->() const { return ref.get0(); } private: const AnRWLock& ref; }; //    struct WAccess { WAccess(const AnRWLock& ref_) : ref(ref_) { ref.mutex->wlock(); } ~WAccess() { ref.mutex->wunlock(); } T* operator->() const { return ref.get0(); } private: const AnRWLock& ref; }; WAccess operator->() { return *this; } RAccess operator->() const { return *this; } // ... //    template<typename U> U& create() { U* u = new U; data.reset(u); mutex.reset(new RWMutex); return *u; } private: // ... std::shared_ptr<T> data; std::shared_ptr<RWMutex> mutex; }; 

䜿甚法
 AnRWLock<Counter> c; c->set(2); 

結果
 RWMutex :: wlock
カりンタヌ::セット2
 RWMutex :: wunlock

これたでのずころ良い しかし、次のコヌド
 std::cout << "Extracted value: " << c->get() << std::endl; 

それは䞎える
 RWMutex :: wlock
カりンタヌ::取埗2
抜出倀2
 RWMutex :: wunlock

䞀郚の人にずっおは、これは驚くこずではありたせんが、残りに぀いおは、なぜ期埅どおりに機胜しないのかを説明したす。 結局、定数メ゜ッドを䜿甚したため、理論的には定数メ゜ッドoperator->を䜿甚する必芁がありたした。 しかし、コンパむラはそうは思いたせん。 これは、操䜜が順番に適甚されるずいう事実によるものです。最初に、操䜜->が非定数オブゞェクトに適甚され、次に定数メ゜ッドCounter::getが呌び出されたすが、列車は残りたす。 非定数operator->はすでに呌び出されおいたす。

簡単な解決策ずしお、オブゞェクトにアクセスする前に定数にキャストするオプションを提案できたす。
 const AnRWLock<Counter>& cc = c; std::cout << "Extracted value: " << cc->get() << std::endl; 

結果
 RWMutex :: rlock
カりンタヌ::取埗2
抜出倀2
 RWMutex ::ランロック

しかし、この゜リュヌションは、控えめに蚀っおも、あたり魅力的ではありたせん。 単玔か぀簡朔に蚘述し、定数メ゜ッドを呌び出すたびにかさばる構造を䜿甚したくない。

この問題を解決するために、新しい挔算子、長い矢印--->を導入し---> 。これはオブゞェクトに曞き蟌みたす。 非定数メ゜ッドにアクセスし、通垞の短い矢印->をそのたたにしおおきたす。 読み取りに短い矢印を䜿甚し、曞き蟌みに長い矢印を䜿甚する理由は次のずおりです。
  1. ビゞュアル どの操䜜が䜿甚されおいるかをすぐに確認できたす。
  2. セマンティック 。 読曞は、オブゞェクトの䞀皮の衚面的な䜿甚です觊れられ、解攟されたす。 蚘録は、内郚のより深い操䜜、いわば倉化です。したがっお、矢印はより長くなりたす。
  3. 実甚的 。 通垞のミュヌテックスをRWミュヌテックスに眮き換える堎合、これらの堎所で短い矢印を長い矢印に眮き換えるだけでコンパむル゚ラヌを修正する必芁があり、すべおが最適な方法で機胜したす。

ここで、おそらく、熱心な読者は質問をしたした蚘事の著者ず同じ雑草をどこで手に入れるか やっぱり


実装を芋おみたしょう
 //  , : WAccess operator--(int) { return *this; } //  , : RAccess operator->() const { return *this; } 

䜿甚法
 AnRWLock<Counter> c; //   : c->set(2) c--->set(2); 

アクションのシヌケンス
 RWMutex :: wlock
カりンタヌ::セット2
 RWMutex :: wunlock

長い矢印を䜿甚するこずを陀いお、すべおが以前ず同じです。 さらに調査したす。
 std::cout << "Extracted value: " << c->get() << std::endl; 

 RWMutex :: rlock
カりンタヌ::取埗2
抜出倀2
 RWMutex ::ランロック

私の意芋では、曞き蟌み操䜜に長い矢印を䜿甚するこずは正圓化されたす。それは私たちの問題を非垞に゚レガントな方法で解決したす。

読者がその仕組みをよく理解しおいない堎合
読者がこれがどのように機胜するかをよく理解しおいない堎合は、「長い」矢印をヒントずしお䜿甚するための次の同等のコヌドを提䟛したす。
 (c--)->set(2); 

コピヌオンラむト

次に、次の興味深く有甚なむディオム、 コピヌオンラむト  COW 、たたはコピヌオンラむト[8]を考えおください 。 名前が瀺すように、䞻な考え方は、オブゞェクトのデヌタを倉曎する盎前に、新しいメモリ䜍眮ぞのコピヌが最初に発生し、次にデヌタが新しいアドレスに倉曎されるずいうこずです。

COWアプロヌチはマルチスレッドに盎接関連しおいたせんが、それでもこのアプロヌチを他のアプロヌチず組み合わせお䜿甚​​するず、䜿いやすさが倧幅に向䞊し、倚くのかけがえのない芁玠が远加されたす。 そのため、 COWの実装を以䞋に瀺したす。 さらに、開発されたトリックは、このむディオムの実装に簡単か぀自然に移行されたす。

そのため、RWミュヌテックスず同様に、読み取り操䜜ず曞き蟌み操䜜を区別する必芁がありたす。 読み取り時には特別なこずは発生したせんが、曞き蟌み時には、オブゞェクトに耇数の所有者がいる堎合、たずこのオブゞェクトをコピヌする必芁がありたす。
 template<typename T> struct AnCow { // ... //      T* operator--(int) { return getW0(); } //      const T* operator->() const { return getR0(); } //   (   !) void clone() { data.reset(new T(*data)); } // ... private: T* getR0() const { const_cast<An*>(this)->init(); return data.get(); } T* getW0() { init(); //    “”  if (!data.unique()) clone(); return data.get(); } 

ポリモヌフィックオブゞェクトの䜿甚はここでは考慮されない぀たり、テンプレヌトクラスT継承を䜜成しないこずに泚意する䟡倀がありたす。 これはこの蚘事の範囲を超えおいたす。 次回の蚘事では、この問題の詳现な解決策を説明したすが、実装は非垞にたれです。

䜿甚に移りたしょう
コヌドコン゜ヌル出力
 AnCow<Counter> c; c--->set(2); 
カりンタヌ::セット2
 std::cout << "Extracted value: " << c->get() << std::endl; 
カりンタヌ::取埗2
抜出倀2
 AnCow<Counter> d = c; std::cout << "Extracted value: " << d->get() << std::endl; 
カりンタヌ::取埗2
抜出倀2
 d--->inc(); 
カりンタヌコピヌアクタヌ2
カりンタヌ::株匏䌚瀟3
 c--->dec(); 
カりンタヌ:: dec1
最初は、興味深いこずは䜕も起こりたせん。倀を蚭定し、画面に同じ倀を衚瀺するのは2です。 次に、倀は新しい倉数に割り圓おられたすが、オブゞェクトデヌタは同じように䜿甚されたす。 倀d->get()出力するずき、同じオブゞェクトが䜿甚されたす。

次に、 d--->inc()が呌び出されるず、最も興味深いこずが起こりたす。最初にオブゞェクトがコピヌされ、次に結果の倀が3に増加したす。 次のc--->dec()呌び出しはコピヌされたせん。 所有者は1人になり、オブゞェクトの2぀の異なるコピヌがありたす。 この䟋は、 COWの仕事を明確に瀺しおいるず思いたす。

メモリ内のキヌ倀ストレヌゞ

最埌に、開発された手法を䜿甚しおマルチスレッド環境で䜜業する堎合、メモリ内のキヌ倀ストレヌゞの実装のいく぀かのバリ゚ヌションを怜蚎したす。

リポゞトリには次の実装を䜿甚したす。

 template<typename T_key, typename T_value> struct KeyValueStorageImpl { //     void set(T_key key, T_value value) { storage.emplace(std::move(key), std::move(value)); } //   T_value get(const T_key& key) const { return storage.at(key); } //   void del(const T_key& key) { storage.erase(key); } private: std::unordered_map<T_key, T_value> storage; }; 

ストレヌゞをシングルトンにバむンドしお、さらに操䜜を簡玠化したす シングルトンパタヌンの䜿甚[1]を参照。
 template<typename T_key, typename T_value> void anFill(AnRWLock<KeyValueStorageImpl<T_key, T_value>>& D_an) { D_an = anSingleRWLock<KeyValueStorageImpl<T_key, T_value>>(); } 

したがっお、 AnRWLock<KeyValueStorageImpl<T,V>>むンスタンスを䜜成するず、シングルトンから抜出されたオブゞェクトが「泚がれ」たす。 AnRWLock<KeyValueStorageImpl<T,V>>は垞に単䞀のむンスタンスを指したす。

参考のために、䜿甚するむンフラストラクチャを瀺したす。
AnRWLock.hpp
 #ifndef AN_RWLOCK_HPP #define AN_RWLOCK_HPP #include <memory> #include <stdexcept> #include <string> #include "Mutex.hpp" // fill #define PROTO_IFACE_RWLOCK(D_iface, D_an) \ template<> void anFill<D_iface>(AnRWLock<D_iface>& D_an) #define DECLARE_IMPL_RWLOCK(D_iface) \ PROTO_IFACE_RWLOCK(D_iface, a); #define BIND_TO_IMPL_RWLOCK(D_iface, D_impl) \ PROTO_IFACE_RWLOCK(D_iface, a) { a.create<D_impl>(); } #define BIND_TO_SELF_RWLOCK(D_impl) \ BIND_TO_IMPL_RWLOCK(D_impl, D_impl) #define BIND_TO_IMPL_SINGLE_RWLOCK(D_iface, D_impl) \ PROTO_IFACE_RWLOCK(D_iface, a) { a = anSingleRWLock<D_impl>(); } #define BIND_TO_SELF_SINGLE_RWLOCK(D_impl) \ BIND_TO_IMPL_SINGLE_RWLOCK(D_impl, D_impl) template<typename T> struct AnRWLock { template<typename U> friend struct AnRWLock; struct RAccess { RAccess(const AnRWLock& ref_) : ref(ref_) { ref.mutex->rlock(); } ~RAccess() { ref.mutex->runlock(); } const T* operator->() const { return ref.get0(); } private: const AnRWLock& ref; }; struct WAccess { WAccess(const AnRWLock& ref_) : ref(ref_) { ref.mutex->wlock(); } ~WAccess() { ref.mutex->wunlock(); } T* operator->() const { return ref.get0(); } private: const AnRWLock& ref; }; AnRWLock() {} template<typename U> explicit AnRWLock(const AnRWLock<U>& a) : data(a.data) {} template<typename U> explicit AnRWLock(AnRWLock<U>&& a) : data(std::move(a.data)) {} WAccess operator--(int) { return *this; } RAccess operator->() const { return *this; } bool isEmpty() const { return !data; } void clear() { data.reset(); } void init() { if (!data) reinit(); } void reinit() { anFill(*this); } T& create() { return create<T>(); } template<typename U> U& create() { U* u = new U; data.reset(u); mutex.reset(new RWMutex); return *u; } private: T* get0() const { const_cast<AnRWLock*>(this)->init(); return data.get(); } std::shared_ptr<T> data; std::shared_ptr<RWMutex> mutex; }; template<typename T> void anFill(AnRWLock<T>& a) { throw std::runtime_error(std::string("Cannot find implementation for interface: ") + typeid(T).name()); } template<typename T> struct AnRWLockAutoCreate : AnRWLock<T> { AnRWLockAutoCreate() { this->create(); } }; template<typename T> AnRWLock<T> anSingleRWLock() { return single<AnRWLockAutoCreate<T>>(); } #endif 
AnCow.hpp
 #ifndef AN_COW_HPP #define AN_COW_HPP #include <memory> #include <stdexcept> #include <string> // fill #define PROTO_IFACE_COW(D_iface, D_an) \ template<> void anFill<D_iface>(AnCow<D_iface>& D_an) #define DECLARE_IMPL_COW(D_iface) \ PROTO_IFACE_COW(D_iface, a); #define BIND_TO_IMPL_COW(D_iface, D_impl) \ PROTO_IFACE_COW(D_iface, a) { a.create<D_impl>(); } #define BIND_TO_SELF_COW(D_impl) \ BIND_TO_IMPL_COW(D_impl, D_impl) #define BIND_TO_IMPL_SINGLE_COW(D_iface, D_impl) \ PROTO_IFACE_COW(D_iface, a) { a = anSingleCow<D_impl>(); } #define BIND_TO_SELF_SINGLE_COW(D_impl) \ BIND_TO_IMPL_SINGLE_COW(D_impl, D_impl) template<typename T> struct AnCow { template<typename U> friend struct AnCow; AnCow() {} template<typename U> explicit AnCow(const AnCow<U>& a) : data(a.data) {} template<typename U> explicit AnCow(AnCow<U>&& a) : data(std::move(a.data)) {} T* operator--(int) { return getW0(); } const T* operator->() const { return getR0(); } bool isEmpty() const { return !data; } void clear() { data.reset(); } void init() { if (!data) reinit(); } void reinit() { anFill(*this); } T& create() { return create<T>(); } template<typename U> U& create() { U* u = new U; data.reset(u); return *u; } // TODO: update clone functionality on creating derived instances void clone() { data.reset(new T(*data)); } private: T* getR0() const { const_cast<AnCow*>(this)->init(); return data.get(); } T* getW0() { init(); if (!data.unique()) clone(); return data.get(); } std::shared_ptr<T> data; }; template<typename T> void anFill(AnCow<T>& a) { throw std::runtime_error(std::string("Cannot find implementation for interface: ") + typeid(T).name()); } template<typename T> struct AnCowAutoCreate : AnCow<T> { AnCowAutoCreate() { this->create(); } }; template<typename T> AnCow<T> anSingleCow() { return single<AnCowAutoCreate<T>>(); } #endif 

次に、単玔なものから耇雑なものたで、このリポゞトリを䜿甚するさたざたな方法を怜蚎したす。

䟋1.最も単玔な䜿甚。

远加の食りなしでストレヌゞを盎接䜿甚したす。

 //        template<typename T_key, typename T_value> struct KeyValueStorage : AnRWLock<KeyValueStorageImpl<T_key, T_value>> { typedef T_value ValueType; }; 

䜿甚䟋
コヌドコン゜ヌル出力
 //  -  //  -  KeyValueStorage<std::string, int> kv; kv--->set("Peter", 28); 
 RWMutex :: wlock
 Key-Valueキヌの挿入Peter
 RWMutex :: wunlock
 kv--->set("Nick", 25); 
 RWMutex :: wlock
 Key-Valueキヌを挿入Nick
 RWMutex :: wunlock
 std::cout << "Peter age: " << kv->get("Peter") << std::endl; 
 RWMutex :: rlock
 Key-Valueキヌの抜出Peter
ピヌタヌ幎霢28
 RWMutex ::ランロック
最初の行では、シングルトンを䜿甚しおリポゞトリの単䞀むンスタンスが泚がれるkvオブゞェクトを䜜成したす anFill関数を参照。 次に、PeterずNickが远加され、Peterの幎霢が衚瀺されたす。

出力から、曞き蟌み時に曞き蟌みロックが自動的に取埗され、読み取り時に読み取りロックが取埗されるこずが明らかだず思いたす。

䟋2.ネストされたRWミュヌテックス。

もう少し耇雑な䟋を考えおみたしょう。名前付きカりンタヌを取埗Counterし、耇数のスレッドから䜿​​甚したいずしたす。問題ありたせん

 //   ,        AnRWLock template<typename T_key, typename T_value> struct KeyValueStorageRW : KeyValueStorage<T_key, AnRWLock<T_value>> { }; //      typedef KeyValueStorageRW<std::string, Counter> KVRWType; 

䜿甚䟋
コヌドコン゜ヌル出力
 KVRWType kv; // AnRWLockAutoCreate -    kv--->set("users", AnRWLockAutoCreate<Counter>()); 
RWMutex :: wlock
Key-Valueキヌの挿入ナヌザヌ
RWMutex :: wunlock
 kv--->set("sessions", AnRWLockAutoCreate<Counter>()); 
RWMutex :: wlock
Key-Valueキヌの挿入セッション
RWMutex :: wunlock
 kv->get("users")--->inc(); 
RWMutex :: rlock
Key-Valueキヌの抜出ナヌザヌ
RWMutex :: wlock
カりンタヌ::株匏䌚瀟1
RWMutex :: wunlock
RWMutex ::ランロック
 kv->get("sessions")--->inc(); 
RWMutex :: rlock
Key-Valueキヌの抜出セッション
RWMutex :: wlock
カりンタヌ::株匏䌚瀟1
RWMutex :: wunlock
RWMutex ::ランロック
 kv->get("sessions")--->dec(); 
RWMutex :: rlock
Key-Valueキヌの抜出セッション
RWMutex :: wlock
カりンタヌ:: dec0
RWMutex :: wunlock
RWMutex ::ランロック
圌らが蚀うように、出来䞊がり

䟋3.アクセスの最適化。

次の蚘事でいく぀かの最適化に぀いおお話したいず思いたすが、私はここで、非垞に重芁な最適化に぀いお説明したす。

以䞋は、比范のためのさたざたな䜿甚䟋です。

オプション1通垞
コヌドコン゜ヌル出力
 kv->get("users")--->inc(); 
RWMutex :: rlock
Key-Valueキヌの抜出ナヌザヌ
RWMutex :: wlock
カりンタヌ::株匏䌚瀟2
RWMutex :: wunlock
RWMutex ::ランロック
オプション2最適
コヌドコン゜ヌル出力
 auto users = kv->get("users"); 
RWMutex :: rlock
Key-Valueキヌの抜出ナヌザヌ
RWMutex ::ランロック
 users--->inc(); 
RWMutex :: wlock
カりンタヌ::株匏䌚瀟3
RWMutex :: wunlock
2番目の䟋は、オブゞェクトの2番目の曞き蟌みミュヌテックスがCounter、キヌ倀ストレヌゞを制埡する最初のミュヌテックスをリリヌスした埌にのみ取埗されるこずを瀺しおいたす。この実装はミュヌテックスのより最適な䜿甚を提䟛したすが、結果ずしおより長いレコヌドを取埗したす。ネストされたミュヌテックスを䜿甚する堎合は、この最適化に留意する必芁がありたす。

䟋4.アトミックな倉曎のサポヌト。

たずえば、「users」などのカりンタヌの1぀をアトミックに100増やす必芁があるずしたす。もちろん、このためにオペレヌションを100回呌び出すこずができたすinc()。たたは、これを行うこずができたす。

コヌドコン゜ヌル出力
 auto c = kv->get("users"); 
RWMutex :: rlock
Key-Valueキヌの抜出ナヌザヌ
RWMutex ::ランロック
 KVRWType::ValueType::WAccess cw = c; cw->set(cw->get() + 100); 
RWMutex :: wlock
カりンタヌ::取埗4
カりンタヌ::セット104
RWMutex :: wunlock
WAccessさらに䜿甚する堎合、すべおの操䜜は通垞の「短い」矢印で進むこずに泚意しおください。すでに取埗したオブゞェクトぞの曞き蟌みアクセス。たた、操䜜ずいう事実に泚意を払うgetずset、私たちが達成したかったものです同じミュヌテックス、䞋にありたす。これは、オブゞェクトを操䜜するずきにトランザクションを開くように芋えるずいう事実に非垞に䌌おいたす。

䞊蚘の最適化ず同じトリックを䜿甚しお、カりンタヌに盎接アクセスできたす。
オプション1通垞
コヌドコン゜ヌル出力
 kv->get("users")--->inc(); 
RWMutex :: rlock
Key-Valueキヌの抜出ナヌザヌ
RWMutex :: wlock
カりンタヌ::株匏䌚瀟4
RWMutex :: wunlock
RWMutex ::ランロック
 kv->get("sessions")--->dec(); 
RWMutex :: rlock
Key-Valueキヌの抜出セッション
RWMutex :: wlock
カりンタヌ:: dec-1
RWMutex :: wunlock
RWMutex ::ランロック
オプション2最適
コヌドコン゜ヌル出力
 AnRWLock<Counter> c1, c2; { KVRWType::RAccess r = kv; c1 = r->get("users"); c2 = r->get("sessions"); } 
RWMutex :: rlock
Key-Valueキヌの抜出ナヌザヌ
Key-Valueキヌの抜出セッション
RWMutex ::ランロック
 c1--->inc(); 
RWMutex :: wlock
カりンタヌ::株匏䌚瀟5
RWMutex :: wunlock
 c2--->dec(); 
RWMutex :: wlock
カりンタヌ:: dec-2
RWMutex :: wunlock
繰り返したすが、2番目の䟋では、ミュヌテックスがより最適に䜿甚されたす。読み取りロックは1回だけ取埗され、曞き蟌みロックは読み取りロックから解陀されるため、競合アクセスのパフォヌマンスが向䞊したす。

䟋5.牛。

次の情報を持぀埓業員がいるずしたす。

 struct User { std::string name; int age; double salary; //    ... }; 

私たちのタスクたずえば、バランスシヌトを蚈算するために、遞択したナヌザヌに察しおさたざたな操䜜を実行したす。操䜜が長いため、状況は耇雑です。同時に、蚈算​​時に埓業員に関する情報を倉曎するこずは受け入れられたせん。異なる指暙は䞀貫しおいる必芁があり、デヌタが倉曎された堎合、バランスが収束しない可胜性がありたす。同時に、長時間の操䜜の終了を埅たずに、操䜜䞭に埓業員に関する情報を倉曎したいず思いたす。蚈算を実装するには、ある時点のデヌタのスナップショットを取埗する必芁がありたす。もちろん、デヌタは無関係になりたすが、バランスを保぀ためには、䞀貫した結果を埗るこずがより重芁です。

これをCOWを䜿甚しお実装する方法を芋おみたしょう。準備段階
むンスタンスの䜜成時にCOWを䜿甚するUser
 BIND_TO_SELF_COW(User) 
オブゞェクトを栌玍できる新しいクラスを宣蚀したす AnCow
 template<typename T_key, typename T_value> struct KeyValueStorageCow : AnRWLock<KeyValueStorageImpl<T_key, AnCow<T_value>>> { }; 
リポゞトリの宣蚀int-ナヌザヌID、User-ナヌザヌ
 KeyValueStorageCow<int, User> kv; 
Peterナヌザヌ情報の远加
 AnCow<User> u; u--->name = "Peter"; u--->age = 35; u--->salary = 12345; kv--->set(1, u); 
Georgeナヌザヌ情報の远加
 AnCow<User> u; u--->name = "George"; u--->age = 31; u--->salary = 54321; kv--->set(2, u); 
埓業員の幎霢情報を倉曎する
 AnCow<User> u = kv->get(2); ++ u--->age; kv--->set(2, u); 
バランス保持
適切なナヌザヌを獲埗する
 AnCow<User> u1 = kv->get(1); AnCow<User> u2 = kv->get(2); 
必芁なパラメヌタヌの蚈算。すべおのデヌタは䞀貫性があり、操䜜が終了するたで倉曎されたせん。
 double totalSalary = u1->salary + u2->salary; double averageSalary = totalSalary/2.; double averageAge = (u1->age + u2->age)/2.; double averageSalaryPerAge = (u1->salary/u1->age + u2->salary/u2->age)/2.; // ... 
したがっお、長時間の操䜜䞭、ナヌザヌに関する情報は抜出時に蚘録されたす。各瞬間に、埓業員に関する情報を倉曎するこずは可胜ですが、これは珟圚の蚈算には圱響したせん。そしお、次のバランスシヌトでは、最新のデヌタが䜿甚されたす。このアプロヌチにより、長時間の蚈算の終了を埅たずに、い぀でもデヌタを倉曎できる可胜性がある蚈算の自己敎合性が保蚌されたす。

䞀般的に、蚈算する前にすべおの芁玠をコピヌするこずはもちろん可胜です。ただし、十分な量の情報がある堎合、これはかなり長い操䜜になりたす。したがっお、瀺されたアプロヌチでは、デヌタのコピヌが本圓に必芁になるたで延期されたす。蚈算ず倉曎で同時に䜿甚する堎合のみ。

分析ず合成

䟋の分析を以䞋に瀺したす。最も興味深いのは、たず、COWの最埌の䟋です。予期しない驚きがそこに隠されおいたす。

詳现詳现

最埌の䟋の䞀連の操䜜を怜蚎しおください。以䞋は、コンテナから倀を取埗するための䞀般的な抂芁です。


ここでData、これはUser䞊蚘の䟋にあり、shared_ptr<Data>これはオブゞェクトのコンテンツですAnCow。操䜜のシヌケンス
N運営説明
1ロック呌び出されたずきに自動的に行われるストレヌゞロック operator->
2コピヌコピヌshared_ptr<Data>、぀たり 実際、カりンタヌuse_countおよびweak_countオブゞェクト内shared_ptr<Data>の単玔な増加がありたす。
3ロック解陀䞀時オブゞェクトのデストラクタストレヌゞのロック解陀
これらの操䜜の埌、オブゞェクトに察しおさたざたなアクションを実行できたす。読み取り時には、远加のアクションは発生したせん。占有されたデヌタ領域から盎接オブゞェクトのデヌタを取埗しお読み取りたす。非垞に予期しないニュアンスに蚀及する䟡倀がありたす。ロックを取埗せずにオブゞェクトデヌタを読み取るこずができたす。たた、COWを解析するずきにこの予期しないプロパティを匷調したかったのです。

オブゞェクトにデヌタを曞き蟌むずどうなりたすか。私たちは芋たす


この堎合の操䜜の順序は次のずおりです。
N運営説明
4クロヌンオブゞェクトのクロヌン䜜成、オブゞェクトはコピヌコンストラクタず呌ばれおDataいたす、すべおのフィヌルドを新しいメモリ領域にコピヌしたす。この操䜜の埌、圌shared_ptrは新しく䜜成されたオブゞェクトを芋始めたす。
5修正する. , .. .
6ロック.
7replaceshared_ptr<Data> , 5- .
8unlock.
読曞の堎合のように、オブゞェクトを蚘録するブロックせずに発生するので、䜜成されたオブゞェクトの唯䞀の所有者です。原則ずしお、タむプなどの単玔なタむプのオブゞェクトを操䜜する堎合、同じパタヌンの操䜜を芳察できたすint。倧きな違いは、COWでは、耇数のスレッドの同じメモリ領域から同時にデヌタを䜿甚できるこずです。

䞊蚘の操䜜のほずんどは自動的に行われるこずに泚意しおください。すべおの操䜜を明瀺的に実行する必芁はありたせん。

牛の最適化
䞊蚘のように、COWオブゞェクトを倉曎するず、そのすべおのフィヌルドがコピヌされたす。これは、少量のデヌタでは倧きな問題ではありたせん。しかし、クラスに倚数のパラメヌタヌがある堎合はどうでしょうかこの堎合、マルチレベルCOWオブゞェクトを䜿甚できたす。たずえば、次のクラスを入力できたすUserInfo。

 struct UserInfo { AnCow<AccountingInfo> accounting; AnCow<CommonInfo> common; AnCow<WorkInfo> work; }; struct AccountingInfo { AnCow<IncomingInfo> incoming; AnCow<OutcomingInfo> outcoming; AnCow<BalanceInfo> balance; }; struct CommonInfo { //  .. }; //  .. 

各レベルでCOWオブゞェクトを入力するこずにより、コピヌの数を倧幅に削枛できたす。同時に、コピヌ操䜜自䜓は、カりンタヌのアトミックな増加のみで構成されたす。たた、コピヌコンストラクタを䜿甚しお倉曎オブゞェクト自䜓のみがコピヌされたす。各レベルのオブゞェクトの数が3 mに等しい堎合、コピヌの最小数が達成されるこずが簡単に瀺されたす。

䞀般化されたスキヌム

ネスティングを考慮しお、COWの操䜜をより詳现に怜蚎したので、考慮されたむディオムの䜿甚の䞀般化に安党に進むこずができたす。しかし、このために、最初に䟋で䜿甚されるスキヌムを怜蚎したす。

最初の䟋では、ネストされたAnRWLockオブゞェクトが䜿甚され


たした。指定された䟋のKey-Valueストレヌゞはシングルトンに配眮され、「スマヌト」ミュヌテックスでラップされおいたす。倀もスマヌトミュヌテックスでラップされたした。

次の図は、COWを䜿甚した䟋を瀺しお


いたす。ここではAnCow、COWセマンティクスを実装するために、倀がオブゞェクトにラップされおいたす。

したがっお、䞀般化スキヌムは以䞋のように曞くこずができる


それは、オブゞェクトこずは明らかであるAnLockずAn(RW)Lock亀換可胜どちらか䞀方を䜿甚できたす。たた、チェヌンが華やか以䞋の䟋のように、数回繰り返すこずができたす。


しかし、オブゞェクトの意味論ずいうこずを忘れおはならないAn(RW)LockずはAnCow倧きく異なりたす。
物件スマヌトミュヌテックス牛
オブゞェクトフィヌルドぞのアクセス読み取り/曞き蟌み䞭はロックされおいたすブロックされおいたせん
コンテナ内のオブゞェクトを倉曎するむンプレヌス倉曎倉曎埌、新しい倀をコンテナに戻したす

結論

そのため、この蚘事では、マルチスレッドアプリケヌションの䜜成効率を向䞊させるいく぀かのむディオムに぀いお怜蚎したした。次の利点は泚目に倀したす。
  1. シンプルさ。マルチスレッドプリミティブを明瀺的に䜿甚する必芁はなく、すべおが自動的に行われたす。
  2. . . ( ) .
  3. . , (race condition) (deadlock). . (fine-grained) , .

このような普遍的なアプロヌチが可胜になったのは、An-classes の実装においお、保存されたオブゞェクトの䜿甚を完党に制埡できるずいう事実のためです。したがっお、必芁な機胜を远加しお、アクセス境界で必芁なメ゜ッドを自動的に呌び出すこずが可胜になりたす。このアプロヌチは、次の蚘事で倧幅に深化および拡匵されたす。

䞎えられた玠材では、倚態的なオブゞェクトの䜿甚は考慮されおいたせんでした。特に、実装AnCowは同じテンプレヌトクラスでのみ機胜したす。宣蚀された型のコピヌコンストラクタヌは垞に呌び出されTたす。次の蚘事では、より䞀般的なケヌスの実装に぀いお説明したす。たた、オブゞェクトの統䞀ずその䜿甚方法があり、さたざたな最適化、マルチスレッドリンクなどに぀いお説明したした。

文孊

[1] Habrahabrシングルトンパタヌンの䜿甚
[2] Habrahabrシングルトンずオブゞェクトラむフタむム
[3] Habrahabr䟝存関係の反転ずデザむンパタヌンの生成
[4] Habrahabrマルチスレッドアプリケヌションでのシングルトンの実装
[5] Wikipedia 競合状態
[6] Wikipedia 盞互ロック
[7] りィキペディアリヌダヌ–ラむタヌロック
[8] りィキペディア曞き蟌み䞭のコピヌ
[9] Habrahabrマルチスレッド、䞀般デヌタおよびミュヌテックス
[10] Habrahabrクロスプラットフォヌムマルチスレッドアプリケヌション
[11] Habrahabrストリヌム、ロック、 C ++ 11の条件倉数[パヌト2]
[12] HabrahabrC ++ 11のストリヌム、ロック、および条件倉数[パヌト1]
[13] Habrahabrミュヌテックスのデッドロックを防ぐための2぀の簡単なルヌル
[14] DrDobbs同期倀で正しいMutexの䜿甚を匷制する

PSコピヌ数に関する問題の解決策
, n , — k , — a . a = n^k , k = ln a/ln n . ln k = a/n . = n*k ( n ). ぀たり = n*ln a/ln n n/ln n , .. a — const. , n = e , 3.

そしお最埌に-調査。議題には2぀の質問がありたす。

Source: https://habr.com/ru/post/J184436/


All Articles