C ++のRust Mutex

こんにちは、Habrahabr!



私はしばしばC ++でプログラムを開発し、彼らが何と言ってもこの言語が大好きです。 おそらく多くの分野で彼がまだ交代していないからでしょう。 しかし、この言語には誰もが知っているように、欠陥がないわけではないため、これらの問題のいくつかを解決するために設計された新しいアプローチ、パターン、またはプログラミング言語にも常に興味を持って従います。



そこで、最近、Rustプログラミング言語に関するStepan Koltsov stepanchegの プレゼンテーションに興味を持って見ましたが、この言語でミューテックスを実装するというアイデアがとても気に入りました。 そして、C ++でのそのようなプリミティブの実装に対する障害は見られず、実際にこれを実装するためにすぐにIDEを開きました。



C ++ 11標準を使用して作成することを-std=c++11



に警告します。したがって、提案されたコードをコンパイルする場合は、 -std=c++11



フラグを使用してこれを行う必要があります。 また、私はオリジナルのふりをしていないことをすぐに警告し、そのようなプリミティブがすでにいくつかのライブラリまたはフレームワークに存在していることを完全に認めます。



それでは始めましょう。 最初に、最終的に何を取得したいかを想像してみましょう。 プロトタイプとして機能するRustの同じmutexは、次のように機能します。これは、保護するデータのタイプによってパラメーター化されるテンプレートクラスです。 つまり、実際には、これは私たち全員が慣れているという意味でのミューテックスではなく、ミューテックス自体だけでなく、それが保護する値も含む一種の保護されたリソースです。 同時に、このタイプは、相互排他ロックを事前にキャプチャしないと、原則として、保護されたデータへのアクセスが不可能になるように設計されています。



したがって、結果は、上記のプロパティを持つ型によってパラメーター化されたテンプレートクラスになります。 条件付きでSharedResourceと呼びましょう。 彼との仕事は、最終的には次のようになります。

コードを表示
 SharedResource<int> shared_int(5); // ... // -   { //      , //         auto shared_int_accessor = shared_int.lock(); *shared_int_accessor = 10; //   ,  shared_int_accessor //        //   }
      
      





ご覧のとおり、すべてがシンプルですが安全です。 このアプローチでは、保護されたリソースにアクセスするときにミューテックスをキャプチャすることを忘れたり、共有リソースでのすべての操作が完了した後にミューテックスをリリースしたりすることはできません。 始めましょう。 それでは、標準のコンストラクタ、デストラクタ、および演算子を含むスタブクラス自体のルーチンから始めましょう。

コードを表示
 template<typename T> class SharedResource { public: SharedResource() = default; ~SharedResource() = default; SharedResource(SharedResource&&) = delete; SharedResource(const SharedResource&) = delete; SharedResource& operator=(SharedResource&&) = delete; SharedResource& operator=(const SharedResource&) = delete; private: };
      
      





これまでのところ、すべてが汚いです。 コピーと移動は今のところ禁止されており、必要または希望が生じた場合は変更します。 しかし、この使用例の行にも記載されている機能はまだ実現していません。

 template<typename T> class SharedResource SharedResource<int> shared_int(5);
      
      





保護するリソースを初期化することはできません。 修正してみましょう。 標準C ++ 03以前を使用する場合、明らかな理由で(可能ですが)かなり問題になります。保護されたリソースの設計者は、任意の型の引数をいくつでも取ることができます。 ただし、C ++ 11のVariadicテンプレートの導入により、この問題は解消されました。 必要なすべての機能は、次のように簡単かつ簡単に実装されます。

コードを表示
 template<typename T> class SharedResource { public: template<typename ...Args> SharedResource(Args ...args) : m_resource(args...) { } ~SharedResource() = default; SharedResource(SharedResource&&) = delete; SharedResource(const SharedResource&) = delete; SharedResource& operator=(SharedResource&&) = delete; SharedResource& operator=(const SharedResource&) = delete; private: T m_resource; };
      
      





これで、クラスにm_resourceフィールドが表示されました-これは、保護するのと同じリソースです。 そして今、私たちにとって都合の良い方法で初期化することができます。 リソースの制御を奪い、それにアクセスする可能性を認識することだけが残っています-つまり、最も興味深いことです。 始めましょう:

コードを表示
 #include <mutex> template<typename T> class SharedResource { public: template<typename ...Args> SharedResource(Args ...args) : m_resource(args...) { } ~SharedResource() = default; SharedResource(SharedResource&&) = delete; SharedResource(const SharedResource&) = delete; SharedResource& operator=(SharedResource&&) = delete; SharedResource& operator=(const SharedResource&) = delete; class Accessor { friend class SharedResource<T>; public: ~Accessor() { m_shared_resource.m_mutex.unlock(); } private: Accessor(SharedResource<T> &resource) : m_shared_resource(resource) { m_shared_resource.m_mutex.lock(); } SharedResource<T> &m_shared_resource; }; Accessor lock() { return Accessor(*this); } private: T m_resource; std::mutex m_mutex; };
      
      





ご覧のとおり、新しいクラスSharedResource :: Accessorがあります。 このクラスは、キャプチャ中に共有リソースへのアクセスを提供するまさにプロキシです。 SharedResourceクラスは、このクラスがそのコンストラクターを呼び出せるように、このクラスに対してフレンドリーであると宣言されています。 重要な点は、親クラス以外の誰もこのクラスを直接インスタンス化できないことです。 これを行う唯一の方法は、SharedResource :: lock()メソッドを呼び出すことです。 また、このクラスのインスタンスを構築すると、ミューテックスがキャプチャされ、破棄されるとリリースされることがわかります。 ここではすべてが明確です-リソースへのアクセスがある間は常にリソースのミューテックスをキャプチャし、その存在はSharedResource :: Accessorクラスによって提供される必要があります。



ただし、現在の状態では、クラスは非常に安全ではありません。 このクラスのインスタンスをコピーまたは移動することです。 最初と2番目のどちらも明示的に宣言されていないため、デフォルトのコンストラクターと演算子が使用されます。 同時に、それらは正しく機能しません。たとえば、コピーするとき、ミューテックスは再びキャプチャされません(これは正しいです)が、破壊されると解放されます。 したがって、クラスのインスタンスがコピーされると、ミューテックスはキャプチャされたよりももう一度解放され、お気に入りの未定義の動作が得られます。 これを修正してみましょう。

コードを表示
 #include <mutex> template<typename T> class SharedResource { public: template<typename ...Args> SharedResource(Args ...args) : m_resource(args...) { } ~SharedResource() = default; SharedResource(SharedResource&&) = delete; SharedResource(const SharedResource&) = delete; SharedResource& operator=(SharedResource&&) = delete; SharedResource& operator=(const SharedResource&) = delete; class Accessor { friend class SharedResource<T>; public: ~Accessor() { if (m_shared_resource) { m_shared_resource->m_mutex.unlock(); } } Accessor(const Accessor&) = delete; Accessor& operator=(const Accessor&) = delete; Accessor(Accessor&& a) : m_shared_resource(a.m_shared_resource) { a.m_shared_resource = nullptr; } Accessor& operator=(Accessor&& a) { if (&a != this) { if (m_shared_resource) { m_shared_resource->m_mutex.unlock(); } m_shared_resource = a.m_shared_resource; a.m_shared_resource = nullptr; } return *this; } private: Accessor(SharedResource<T> *resource) : m_shared_resource(resource) { m_shared_resource->m_mutex.lock(); } SharedResource<T> *m_shared_resource; }; Accessor lock() { return Accessor(this); } private: T m_resource; std::mutex m_mutex; };
      
      





コピーは禁止されていますが、移動は許可されています。 この決定のマイナスの結果は、プロキシが(移動後に)無効になり、リソースへのアクセスに使用できないことです。 これはあまり良くありませんが、致命的ではありません-移動されたオブジェクトはそれ以上の使用を意図していません。 さらに、nullptrのおかげで、そのようなオブジェクトを使用した場合のクラッシュは100%の場合に再現されます。これにより、ほとんどの場合、このようなエラーの検出はそれほど難しくありません。 それでも、ユーザーにオブジェクトの有効性を確認する機会を与えるといいでしょう。 これを行うには、このメソッドを追加します。

コードを表示
 bool isValid() const noexcept { return m_shared_resource != nullptr; }
      
      





これで、ユーザーはプロキシのコピーの有効性をいつでも確認できます。 必要に応じて、演算子boolを追加できますが、これは行いませんでした。 そのため、共有リソースへのアクセスのみを実装することに変わりはありません。 これを行うには、SharedResource :: Accessorクラスに次のステートメントを追加します。

コードを表示
 T* operator->() { return &m_shared_resource->m_resource; } T& operator*() { return m_shared_resource->m_resource; }
      
      





クラス全体は次のようになります。

コードを表示
 #include <mutex> template<typename T> class SharedResource { public: template<typename ...Args> SharedResource(Args ...args) : m_resource(args...) { } ~SharedResource() = default; SharedResource(SharedResource&&) = delete; SharedResource(const SharedResource&) = delete; SharedResource& operator=(SharedResource&&) = delete; SharedResource& operator=(const SharedResource&) = delete; class Accessor { friend class SharedResource<T>; public: ~Accessor() { if (m_shared_resource) { m_shared_resource->m_mutex.unlock(); } } Accessor(const Accessor&) = delete; Accessor& operator=(const Accessor&) = delete; Accessor(Accessor&& a) : m_shared_resource(a.m_shared_resource) { a.m_shared_resource = nullptr; } Accessor& operator=(Accessor&& a) { if (&a != this) { if (m_shared_resource) { m_shared_resource->m_mutex.unlock(); } m_shared_resource = a.m_shared_resource; a.m_shared_resource = nullptr; } return *this; } bool isValid() const noexcept { return m_shared_resource != nullptr; } T* operator->() { return &m_shared_resource->m_resource; } T& operator*() { return m_shared_resource->m_resource; } private: Accessor(SharedResource<T> *resource) : m_shared_resource(resource) { m_shared_resource->m_mutex.lock(); } SharedResource<T> *m_shared_resource; }; Accessor lock() { return Accessor(this); } private: T m_resource; std::mutex m_mutex; };
      
      





できた このクラスのすべての基本機能が実装され、クラスを使用する準備が整いました。 もちろん、クラスを作成するときに、mutexを転送された条件変数(condvars)のリストに関連付けるRustのメソッドnew_with_condvarsの類似物を実装すると便利です。 C ++では、condvarインスタンスを待機しているとき、ミューテックスと条件変数のバインドが異なります。 これを行うには、unique_lockクラスのインスタンスがcondition_variable :: waitメソッドに渡されます。これは、リソースへのアクセスを提供せずにmutexの所有権を抽象化したものです。



condvarsとの相互作用が可能になるように実装を変更することは可能ですが、この場合、実装は単純で信頼性がなくなり、これがまさに当初の意図でした。 ただし、希望する人はcondvarsで動作する実装を以下で見つけることができます。

コードを表示
 #include <mutex> template<typename T> class SharedResource { public: template<typename ...Args> SharedResource(Args ...args) : m_resource(args...) { } ~SharedResource() = default; SharedResource(SharedResource&&) = delete; SharedResource(const SharedResource&) = delete; SharedResource& operator=(SharedResource&&) = delete; SharedResource& operator=(const SharedResource&) = delete; class Accessor { friend class SharedResource<T>; public: ~Accessor() = default; Accessor(const Accessor&) = delete; Accessor& operator=(const Accessor&) = delete; Accessor(Accessor&& a) : m_lock(std::move(a.m_lock)), m_shared_resource(a.m_shared_resource) { a.m_shared_resource = nullptr; } Accessor& operator=(Accessor&& a) { if (&a != this) { m_lock = std::move(a.m_lock); m_shared_resource = a.m_shared_resource; a.m_shared_resource = nullptr; } return *this; } bool isValid() const noexcept { return m_shared_resource != nullptr; } T* operator->() { return m_shared_resource; } T& operator*() { return *m_shared_resource; } std::unique_lock<std::mutex>& get_lock() noexcept { return m_lock; } private: Accessor(SharedResource<T> *resource) : m_lock(resource->m_mutex), m_shared_resource(&resource->m_resource) { } std::unique_lock<std::mutex> m_lock; T *m_shared_resource; }; Accessor lock() { return Accessor(this); } private: T m_resource; std::mutex m_mutex; };
      
      





これにより、クラスの実装が完了したと考えることができます。

さて、このクラスをどのように使用してはいけないかについて、自分自身を足で撃ったり、他の重要なことをしないようにするためのコメントをいくつか:





ご清聴ありがとうございました。

githubコードへのリンク: https : //github.com/isapego/shared-resource

コードはパブリックドメインライセンスで公開されているため、思い浮かぶことなら何でもできます。

この記事が誰かに役立つとうれしいです。



PS

エラーを指摘し、こことgithubでアドバイスを提供し、記事とコードの改善に協力してくれたすべての人に感謝します。



All Articles