RAIIイディオムとロックについて

イディオムRAII(Resource Acquisition Is Initialization)はC ++言語に由来し、一部のリソースがオブジェクトのコンストラクターでキャプチャーされ、そのデストラクターで解放されるという事実にあります。 また、理由(通常のメソッドの終了または例外がスローされた場合)に関係なく、メソッドを終了する(またはスコープ外に出る)ときにローカルオブジェクトデストラクタが自動的に呼び出されるため、このイディオムを使用することは、ポイントから安全なC ++コードを記述する最も簡単で効果的な方法です例外を表示します。



.NETやJavaなどの「管理された」プラットフォームに切り替えると、ガベージコレクターはメモリの解放に関与しているため、このイディオムは何らかの意味でその関連性を失います。つまり、メモリはC ++で処理しなければならない最も一般的なリソースでした。 ただし、ガベージコレクターはメモリのみを処理し、リソース(オペレーティングシステムハンドルなど)の確定的なリリースに寄与しないため、RAIIイディオムは.NETとJavaの両方で使用されますが、この複雑な名前についてはほとんど開発者が知りません。



C#言語の最初のバージョンから、 を使用して自由に構築できるようになりました。これは、 Disposeメソッドを呼び出すことでリソースの自動解放を提供します。 リソースを確定的に解放する別の方法は、 try / finallyブロックを手動で使用することでした(そして現在も残っています)。 「リーダー」と「ライター」の間で共有リソースをより効果的に共有するように設計されたReaderWriterLockSlimクラスの使用の次の簡単な例を見てみましょう。







ManualLockManagermentメソッドは手動ロック制御を使用しますが、 UsingBasedMethodメソッドは小さなシェルに基づいています。 このシェルの完全な例はここにありますが 、その仕組みを推測することは難しくありません。UseReadLock拡張メソッドは、コンストラクターが読み取りロックをキャプチャし、 Disposeメソッドがそれを解放するオブジェクトを作成します。 問題は、2つのフラグメントがどれだけ同等であるか、何を優先すべきかということです。 もちろん、 usingブロックを使用した「自転車」は読みやすいように見えますが、これだけが違いますか?



わかった 最初の例を少し複雑にしましょう。 コードが読み取りロックをロックした後、読み取りロックを再度ロックするメソッドを呼び出すときに、再帰呼び出しの可能性がある場合はどうでしょうか。 すべての読者が再キャプチャ(デフォルトでは再入可能モード)の観点からReaderWriterLockSlimオブジェクトの動作を覚えているとは思わないので、 ロックコンストラクトとは異なり、 ReaderWriterLockSlimオブジェクトデフォルトで再帰キャプチャをサポートしていません。







この場合、ロックオブジェクトの不一致状態とSynchronizationLockException例外の生成を取得しますが、それをスローするコード内の正確なポイントはどれほど明白で、何につながるのでしょうか? 上記のコードの問題は、RAIIイディオムとブロックの使用の動作と一致しないことです。 リソースは、 前に正常にキャプチャされた場合にのみ finally ブロックで解放する必要があります



この場合、以下が発生します: ReaderWriterLockSlimオブジェクト再帰的なキャプチャをサポートしていないため、 EnterReadLockメソッドをもう一度呼び出すと( AnotherMethodメソッドの3行目)、 LockRecusionExceptionスローされますが、この呼び出しはtryブロック内にあるため、 finallyMethodブロックが呼び出されますExitReadLockへの後続の呼び出し。 その結果、4行目で既にフリーロックを取得していますが、それをキャプチャしなかったため、それ自体は正常ではありません。 その後、制御がSomeMethodメソッドに返され、 finallyブロックに移動します。ここでExitReadLockメソッドが再度呼び出されます。



デッドロック およびその他のトラブル





ここからが楽しみです。 try / finallyを使用しリソースを手動で管理するためにusing vs構文を使用する問題について考えたとき、このコードが初めて行で例外を伴ってドロップし、その後SomeMethodメソッドの行2に再び落ちると想定しました 。 私はこのように推論しました: ReaderWriterLockSlimは再帰的なキャプチャをサポートしていないため、元の例外は3行目で発生しますが、最初のメソッドでロックを再度解除しようとすると、4行目でロックが解除されるため、元の例外を「マスク」する別の例外が生成されます。



このような人工的な例では、この動作により、本当の理由を見つけるのは非常に簡単になりますが、実稼働サーバーでのそのような動作は、コードがまったく明白ではないかもしれないため、必然的にキャプチャされるときにロックがキャプチャされないと言うので、あなたの血を冷やすことができますメソッドの始まり。

ただし、実際には、動作はわずかに異なり、より正確には、.NET 4.5の場合とまったく同じになりますが、プラットフォームの以前のバージョンでは完全に異なります。 順番に行きましょう。



問題は、.NET 4.0(およびそれ以降)での動作が次のようになることです。EnterReadLockメソッドを呼び出す前にExitReadLockメソッドを呼び出そうとすると例外がスローされますが、 EnterReadLockの 1回の呼び出し後にExitReadLockを 2回呼び出すと成功します!







この問題で最も不愉快なのは、 現在の古いバージョンのフレームワークでは、このコードが成功するだけでなく、ロックオブジェクトの状態の不一致につながることです。その結果、コンソールに次のように表示されます。 実際、3行目ではロックカウンターの値を0に減らし、4行目では再びそれを減らしました。 その結果、カウンターは-1になり、4294967295は符号なし形式での値「-1」の単なる表現になります。 しかし、最も重要なことは、愚か者が読み取りロックがキャプチャされたと仮定するため、書き込みロックをキャプチャする後続の試行が永久にスタックすることです。



判明したように、これは.NET Frameworkの既知のバグであり、最終的に.NET 4.5で修正されました。 VS2012をインストールした後、非常に快適ではありませんが、予想される動作が得られます。ロックが読み取りのために再キャプチャされたときに発生した元の例外は、キャプチャされていないロックを解放しようとするときに発生する新しい例外によってマスクされます!



リソースの 使用 またはキャプチャを正しく 使用 して ください





usingブロックがどのように機能するかを思い出しましょう:







using構文は、 tryブロックの前にリソースのキャプチャが発生するように展開されるため、例外がキャッチされたときに例外がスローされると、リリースは実行されません。 using構文がオブジェクト初期化子と組み合わされている場合、この動作は不快な結果につながる可能性があります(詳細については、usingブロックのオブジェクト初期化子を参照 )が、オブジェクトが正常に作成された場合にのみデストラクタが呼び出されるC ++のコンストラクタ/デストラクタの動作に完全に対応します。





ここで、C ++のデストラクタとC#のファイナライザのさらに別の違いに直面しています。 デストラクタとは異なり、ファイナライザは、作成されたオブジェクトのコンストラクタが例外で落ちた場合でも呼び出されます。 この動作は、C#の例外の観点から安全なリソース管理コードの作成を単純化するため、非常に論理的です。ファイナライザでは、 nullIntPtrZeroなどのリソースキャプチャの成功を確認するだけで十分です。



using構文を使用する場合、リソースのキャプチャが成功した場合にのみDisposeメソッドが呼び出されるため、次のコードは可能な限り予測どおりに動作します。 .NET Frameworkのすべてのバージョンでの通常の状態:










All Articles