知識ベース

リソース管理(コンピューティング)

コンピュータープログラミングでは、 リソース管理とは、リソース(可用性が制限されているコンポーネント)を管理する手法のことです。

コンピュータープログラムは、プログラミング言語(Elder、Jackson&Liblit(2008)はさまざまなアプローチと対照的な調査記事です)によって公開される機能を使用して独自のリソースを管理するか、ホスト(オペレーティングシステムまたは仮想マシン)によって管理することを選択できます。別のプログラム。

ホストベースの管理はリソーストラッキングと呼ばれリソースリークのクリーンアップで構成されています。取得されたが使用後に解放されていないリソースへのアクセスを終了します。これはリソースの再利用と呼ばれ、メモリのガベージコレクションに似ています。多くのシステムでは、プロセスが出口システム呼び出しを行った後、オペレーティングシステムがリソースを再利用します。

アクセスを制御する

プロセスが使用を終了したときにリソースの解放を拒否する行為は、リソースリークと呼ばれ、シーケンシャルコンピューティングの問題です。複数のプロセスが限られたリソースにアクセスしたい場合、コンカレントコンピューティングの問題になる可能性があり、リソース競合として知られています。

リソース管理は、これらの両方の状況を防ぐためにアクセスを制御しようとします。

リソースリーク

正式には、リソース管理(リソースリークの防止)は、リソースが正常に取得された場合にのみリソースが解放されるようにすることで構成されます。この一般的な問題は、通常のようにかどうかに関係なく、本体のコードの、そして唯一のコードが正常に完了する前であれば場合、コードが呼び出された後という条件で、この順に実行されるコードを、「 前に、 身体、そして後に 」抽象化することができます正常に実行されるかどうか。これは、 アラウンド実行またはコードサンドウィッチとも呼ばれ、プログラムの状態の一時的な変更やサブルーチンへの入り口と出口のトレースなど、他のさまざまなコンテキストで発生します。ただし、リソース管理は最もよく引用されるアプリケーションです。アスペクト指向プログラミングでは、このようなロジックの実行はアドバイスの形式です。

制御フロー分析の用語では、リソースの解放がリソース獲得の成功を支配する必要があります。これがバグであることを確認できないと、この条件に違反するコードパスによりリソースリークが発生します。多くの場合、リソースリークは小さな問題であり、通常はプログラムをクラッシュさせるのではなく、プログラムまたはシステム全体の速度を低下させます。ただし、 リソースの枯渇により、プログラム自体または他のプログラムのいずれかでクラッシュが発生する可能性があります。システムのリソースが不足すると、取得要求は失敗します。これにより、攻撃によってリソースが枯渇する可能性がある場合、セキュリティバグが発生する可能性があります。リソースリークは、通常のプログラムフロー(単純にリソースの解放を忘れるなど)で発生する場合や、プログラムの別の部分に例外がある場合にリソースが解放されない場合などの例外的な状況でのみ発生します。リソースリークは、サブルーチンからの早期終了、returnステートメント、またはサブルーチン自体または呼び出されるより深いサブルーチンのいずれかによって発生した例外によって非常に頻繁に発生します。 returnステートメントによるリソースの解放は、戻る前にサブルーチン内で慎重に解放することで処理できますが、例外は、リリースコードの実行を保証する追加の言語機能なしでは処理できません。

さらに微妙に、成功したリソース獲得はリソース解放を支配する必要があります。さもないと、コードは獲得していないリソースを解放しようとします。このような誤ったリリースの結果は、黙って無視されることから、プログラムのクラッシュや予期しない動作にまで及びます。これらのバグは、最初に失敗するためにリソースの割り当てを必要とするため、一般にめったに現れません。これは通常、例外的なケースです。さらに、重要なリソースの取得に失敗したためにプログラムが既にクラッシュしている可能性があるため、結果は深刻ではないかもしれません。ただし、これらは障害からの回復を妨げたり、正常なシャットダウンを無秩序なシャットダウンに変えたりする可能性があります。この条件は、通常、リソースが取得されることを記録するブール変数を持つことにより、リソースが取得される前にリソースが正常に取得されたことを確認することによって保証されます。 –または、null許容型であるリソースへのハンドルによって。ここで、「null」は「正常に取得されていない」ことを示し、原子性を保証します。

リソースの競合

メモリ管理

メモリはリソースとして扱うことができますが、主にメモリの割り当てと割り当て解除はファイルハンドルなどの他のリソースの取得と解放よりもかなり頻繁に行われるため、メモリ管理は通常個別に考慮されます。 外部システムによって管理されるメモリは、(内部)メモリ管理(メモリであるため)とリソース管理(外部システムによって管理されるため)の両方に類似しています。例には、ネイティブコードを介して管理され、Javaから(Java Native Interfaceを介して)使用されるメモリが含まれます。 JavaScriptから使用されるDocument Object Model(DOM)のオブジェクト。どちらの場合も、ランタイム環境(仮想マシン)のメモリマネージャー(ガベージコレクター)は外部メモリを管理できないため(共有メモリ管理はありません)、外部メモリはリソースとして扱われ、同様に管理されます。ただし、システム間のサイクル(JavaScriptはDOMを指し、JavaScriptを指す)により、管理が困難または不可能になる可能性があります。

スタック管理とヒープ管理

プログラム内のリソース管理における重要な違いは、 スタック管理ヒープ管理です。リソースをスタック変数のように処理できるかどうか(ライフタイムは、単一のスタックフレームに制限され、特定のスコープに入るときまたは特定のスコープ内で取得され、リリースされる実行がそのスコープを終了するとき)、またはリソースをヒープ変数のように処理する必要があるかどうか(関数内で取得したリソースから取得し、取得関数の外部で解放する必要があるなど)。スタック管理は一般的な使用例であり、ヒープ管理よりもはるかに簡単に処理できます。

基本的なテクニック

リソース管理の基本的なアプローチは、リソースを取得し、それを使って何かを実行し、それをリリースして、次の形式のコードを生成することです(Pythonでファイルを開く場合を示します)。

def work_with_file(filename):f = open(filename)... f.close()

これは、介在する...コードに早期終了(戻り)が含まれておらず、言語に例外がなく、openが成功することが保証されている場合に正しいです。ただし、戻り値または例外がある場合はリソースリークが発生し、オープンに失敗すると未取得のリソースが誤って解放されます。

さらに2つの基本的な問題があります。取得とリリースのペアは隣接しておらず(リリースコードは取得コードから遠く離れて記述する必要があります)、リソース管理はカプセル化されていません。これらを組み合わせると、取得とリリースは明示的にペアにする必要がありますが、一緒に配置することはできないため、これらを正しくペアにしないことが容易になります。

リソースリークは、bodyをtry句に配置し、リリースをfinally句に配置することにより、finally構築をサポートする言語(Pythonなど)で解決できます。

def work_with_file(filename):f = open(filename)try:... finally:f.close()

これにより、本体内に戻りがあったり、例外がスローされたりした場合でも、正しいリリースが保証されます。さらに、取得はtry句の前に行われ、finally句は、開いているコードが成功した場合(例外をスローせずに)のみ実行されることに注意してください。 Python)。 nullのフォームを返すなど、例外をスローせずにリソースの取得が失敗する可能性がある場合は、次のようにリリース前に確認する必要があります。

def work_with_file(filename):f = open(filename)try:... finally:if f:f.close()

これにより正しいリソース管理が保証されますが、隣接関係またはカプセル化の提供に失敗します。多くの言語には、Pythonのwithステートメントなど、カプセル化を提供するメカニズムがあります。

def work_with_file(filename):with open(filename)as f:...

上記の手法(アンワインド保護(最終)および何らかのカプセル化)は、C#、Common Lisp、Java、Python、Ruby、Scheme、Smalltalkなどのさまざまな形式で見られる最も一般的なリソース管理アプローチです。それらは、LILのNIL方言で1970年代後半のものです。例外処理§履歴を参照してください。実装には多くのバリエーションがあり、大幅に異なるアプローチもあります。

アプローチ

巻き戻し保護

言語間のリソース管理への最も一般的なアプローチは、アンワインド保護を使用することです。これは、実行がスコープを出るときに呼び出されます。ブロックの終わりから実行する、ブロック内から戻る、またはスローされる例外によって実行されますこれは、スタック管理リソースで機能し、C#、Common Lisp、Java、Python、Ruby、Schemeなどの多くの言語で実装されています。このアプローチの主な問題は、リリースコード(ほとんどの場合、finally句)が取得コードから非常に離れている可能性があり( 隣接関係がない )、取得コードとリリースコードは常に呼び出し元によってペアにする必要があることです(不足しています) カプセル化 )。これらは、クロージャ/コールバック/コルーチン(Common Lisp、Ruby、Scheme)を使用するか、取得と解放の両方を処理するオブジェクトを使用して、機能的に修正することができます。スコープ(C#使用、Java try-with-resources、Python with);下記参照。

別の、より必須のアプローチは、非同期スタイルを直接スタイルで記述することです。リソースを取得し、次の行で遅延リリースを取得します。これは、スコープが終了したときに呼び出されます。これは、2000年にAndrei AlexandrescuとPetru MargineanによってScopeGuardクラスとしてC ++で作成され、Joshua Lehrerによって改善され、さらに例外の安全性への1つのアプローチであるscopeキーワード(ScopeGuardStatement)を介してDで直接言語サポートされていますRAIIへ(下記参照)。延期文としてGoにも含まれています。このアプローチにはカプセル化がありません-取得と解放を明示的に一致させる必要がありますが、リソースごとにオブジェクトを作成する必要はありません(コードごとに、リソースのタイプごとにクラスを記述することは避けてください)。

オブジェクト指向プログラミング

オブジェクト指向プログラミングでは、値がファイル記述子(またはより一般的なファイルハンドル)であるフィールドを持つファイルオブジェクトなど、リソースはそれらを使用するオブジェクト内にカプセル化されます。これにより、オブジェクトのユーザーがリソースを使用および管理する必要がなくなります。ただし、オブジェクトとリソースを関連付けることができるさまざまな方法があります。

まず、所有権の問題があります。オブジェクトはリソースがありますか?

  • オブジェクトはリソースを所有できます(オブジェクト構成を介して、強い「持っている」関係)。
  • オブジェクトはリソースを表示できます (オブジェクトの集約により、弱い「持っている」関係)。
  • オブジェクトは、リソースを持つ他のオブジェクトと通信できます(関連付けを使用)。

リソースを持つオブジェクトは、オブジェクトの有効期間中のさまざまな時点で、さまざまな方法でリソースを取得および解放できます。これらはペアで発生しますが、実際には対称的に使用されないことがよくあります(以下を参照)。

  • オブジェクトが有効なときに、openやdisposeなどの(インスタンス)メソッドを使用して取得/解放します。
  • オブジェクトの作成/破棄中に取得/解放します(イニシャライザーとファイナライザーで)。
  • リソースを取得も解放もせず、代わりに、依存関係の注入のように、オブジェクトの外部で管理されるリソースへのビューまたは参照を単に保持します。具体的には、リソースを持っている(または持っているオブジェクトと通信できる)オブジェクトが引数としてメソッドまたはコンストラクターに渡されます。

最も一般的なのは、オブジェクトの作成中にリソースを取得し、一般にdisposeと呼ばれるインスタンスメソッドを介してリソースを明示的に解放することです。これは、従来のファイル管理(オープン中に取得、明示的なクローズによる解放)に類似しており、廃棄パターンとして知られています。これは、Java、C#、Pythonなど、現代の主要なオブジェクト指向言語で使用される基本的なアプローチであり、これらの言語にはリソース管理を自動化するための追加の構造があります。ただし、これらの言語でも、以下で説明するように、オブジェクトの関係が複雑になると、リソース管理が複雑になります。

RAII

自然なアプローチは、リソースの保持をクラス不変にすることです。リソースはオブジェクトの作成中に取得され(具体的には初期化)、オブジェクトの破棄中に解放されます(具体的にはファイナライズ)。これはResource Acquisition Is Initialization(RAII)と呼ばれ、リソース管理をオブジェクトの有効期間に結び付けて、ライブオブジェクトに必要なすべてのリソースを確保します。他のアプローチでは、リソースをクラス不変に保持しないため、オブジェクトが必要なリソースを持たない場合があります(まだ取得されていないか、すでにリリースされているか、外部で管理されているため)。閉じたファイルから。このアプローチは、リソース管理をメモリ管理(具体的にはオブジェクト管理)に結び付けるため、メモリリークがない(オブジェクトリークがない)場合、リソースリークはありません。 RAIIは、スタック管理されたリソースだけでなく、ヒープ管理されたリソースに対しても自然に機能し、構成可能です:任意の複雑な関係(複雑なオブジェクトグラフ)のオブジェクトによって保持されたリソースは、オブジェクト破壊によって透過的に解放されます(これが適切に行われている限り! )。

RAIIはC ++の標準のリソース管理アプローチですが、アピールにもかかわらずC ++以外ではほとんど使用されません。これは、最新の自動メモリ管理、特にガベージコレクションのトレースではうまく機能しないためです。RAII リソース管理をメモリ管理に関連付けますが、これらには大きな違いがあります。まず、リソースは高価であるため、すぐに解放することが望ましいため、リソースを保持しているオブジェクトは、不要になった(使用されなくなった)すぐに破棄する必要があります。オブジェクトの破棄は、C ++(スタックの割り当て解除時にスタックに割り当てられたオブジェクトが破棄され、ヒープの割り当てられたオブジェクトがdeleteを呼び出して手動で破棄されるか、unique_ptrを使用して自動的に破棄される)や、確定的な参照カウント(オブジェクトが破棄される)参照カウントが0になった直後)、したがって、RAIIはこれらの状況でうまく機能します。ただし、最新の自動メモリ管理のほとんどは非決定的であり、オブジェクトが即座に、またはまったく破壊されるという保証はありません!これは、ガベージになるとすぐに各オブジェクトを正確に収集するよりも、ガベージを割り当てたままにしておくほうが安価だからです。オブジェクトは単純に割り当て解除することはできません- -大幅に複雑化し、ガベージコレクションを遅らせる第二に、オブジェクトの破壊時にリソースを解放すると、オブジェクトが( デストラクタとして知られている決定論的なメモリ管理で) ファイナライザを持っていなければならないことを意味します。

複雑な関係

複数のオブジェクトが単一のリソースに依存している場合、リソース管理は複雑になる可能性があります。

基本的な質問は、「持っている」関係が別のオブジェクトの所有 (オブジェクト構成)であるか、別のオブジェクトの表示 (オブジェクトの集約)であるかです。一般的なケースは、パイプおよびフィルターパターン、委任パターン、デコレーターパターン、またはアダプターパターンのように、1つ2つのオブジェクトが連鎖している場合です。 2番目のオブジェクト(直接使用されない)がリソースを保持している場合、最初のオブジェクト(直接使用される)がリソースの管理を担当しますか?これは通常、最初のオブジェクト 2番目のオブジェクトを所有しているかどうかと同じように回答されます。 所有している場合、所有オブジェクトはリソース管理も担当します(「リソースを持っている」は推移的です)、そうでない場合はそうではありません。さらに、単一のオブジェクトが他のいくつかのオブジェクトを「所有」し、いくつかのオブジェクトを所有し、他のオブジェクトを表示する場合があります。

両方のケースがよく見られ、慣習は異なります。間接的にリソースを使用するオブジェクトにリソース(組成)の責任を負わせることでカプセル化(クライアントが使用するオブジェクトのみが必要であり、リソースに個別のオブジェクトは必要ありません)が、特にリソースが複数のオブジェクトまたはオブジェクトには複雑な関係があります。リソースを直接使用するオブジェクトのみがリソース(集約)を担当する場合、リソースを使用する他のオブジェクト間の関係は無視できますが、カプセル化はありません(オブジェクトを直接使用する以外):リソースは直接管理する必要があります。間接的に使用しているオブジェクトが利用できない場合があります(個別にリリースされている場合)。

実装面では、オブジェクトの構成において、破棄パターンを使用する場合、所有オブジェクトにも破棄メソッドがあり、破棄する必要がある所有オブジェクトの破棄メソッドを呼び出します。 RAIIでは、これは自動的に処理されます(所有オブジェクト自体が自動的に破棄される限り:C ++では、値またはunique_ptrであり、未加工のポインターではない場合:ポインターの所有権を参照してください)。オブジェクトの集約では、表示オブジェクトはリソースに対して責任を負わないため、表示オブジェクトで何もする必要はありません。

両方ともよく見られます。たとえば、Javaクラスライブラリでは、Reader#close()が基になるストリームを閉じ、これらをチェーン化できます。たとえば、BufferedReaderにはInputStreamReaderを含めることができます。InputStreamReaderにはFileInputStreamが含まれ、BufferedReaderでcloseを呼び出すとInputStreamReaderが閉じ、FileInputStreamが閉じます。これによりシステムファイルリソースが解放されます。実際、リソースを直接使用するオブジェクトは、カプセル化のおかげで匿名にすることもできます。

try(BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(fileName)))){//リーダーを使用します。 } // try-with-resourcesブロックが終了すると、リーダーが閉じられ、含まれる各オブジェクトが順番に閉じられます。

ただし、リソースを直接使用するオブジェクトのみを管理し、ラッパーオブジェクトでリソース管理を使用しないこともできます。

try(FileInputStream stream = new FileInputStream(fileName)))){BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); //リーダーを使用します。 } // try-with-resourcesブロックが終了すると、ストリームは閉じられます。 //ストリームが閉じられた後、リーダーは使用できなくなりますが、ブロックをエスケープしない限り、これは問題ではありません。

対照的に、Pythonでは、csv.readerは読み込んでいるファイルを所有していないため、リーダーを閉じる必要はなく(また不可能です)、代わりにファイル自体を閉じる必要があります。

open(filename)as f:r = csv.reader(f)#rを使用します。 #fはwith文が終了すると閉じられ、使用できなくなります。 #rには何も行われませんが、基になるfは閉じられているため、rも使用できません。

.NETでは、慣例として、リソースの直接ユーザーにのみ責任を負わせます。「IDisposableは、型がアンマネージリソースを直接使用する場合にのみ実装する必要があります。」

リソースを共有する複数のオブジェクト、またはリソースを保持するオブジェクト間のサイクルなど、より複雑なオブジェクトグラフの場合、適切なリソース管理は非常に複雑になる可能性があり、オブジェクトファイナライズ(デストラクタまたはファイナライザ経由)とまったく同じ問題が発生します;たとえば、オブザーバーパターンを使用している場合(およびオブザーバーがリソースを保持している場合)、リスナーの期限切れの問題が発生し、リソースリークが発生する可能性があります。リソース管理の制御を強化するためのさまざまなメカニズムがあります。たとえば、Google Closure Libraryでは、goog.DisposableクラスはregisterDisposableメソッドを提供し、このオブジェクトと共に破棄される他のオブジェクトを登録します。また、破棄を管理するさまざまな下位インスタンスとクラスメソッドも提供します。

構造化プログラミング

構造化プログラミングでは、すべてのケースを処理するためにコードを十分にネストするだけで、スタックリソース管理が行われます。連続したから起因する三角形状に、アローアンチパターン -これは、コードの末尾に一つだけのリターンを必要とし、多くのリソースを取得する必要がある場合は重く、ネストされたコードにつながることができ、いくつかのことでアンチパターンとみなされますネスティング。

クリーンアップ句

早期復帰を可能にするが、クリーンアップを1か所に統合​​するもう1つのアプローチは、クリーンアップコードが先行する関数の単一の終了リターンを使用し、gotoを使用して終了前にクリーンアップにジャンプすることです。これは現代のコードではめったに見られませんが、Cの一部の使用で発生します。