Javaと.NETには、メモリを管理する素晴らしいガベージコレクターと、外部オブジェクトをすばやく解放するための便利なパターンがあります( Closeable
、 IDisposable
)、ただし、それらが単一のオブジェクトによって所有されている場合のみ。一部のシステムでは、リソースは2つのコンポーネントによって独立して消費され、両方のコンポーネントがリソースを解放するときにのみ解放される必要がある場合があります。
最新のC++では、shared_ptr
を使用してこの問題を解決します。これにより、すべてのshared_ptr
が破棄されたときにリソースが確定的に解放されます。
オブジェクト指向で非決定的にガベージコレクションされたシステムで単一の所有者がいない高価なリソースを管理および解放するための文書化された実証済みのパターンはありますか?
一般に、管理されていない言語であっても、所有者を1人にすることでそれを回避できます。
しかし、原則はマネージ言語でも同じです。 Close()
の高価なリソースをすぐに閉じる代わりに、0に到達するまでカウンター(Open()
/Connect()
/etcでインクリメント)をデクリメントします。実際にクローズします。おそらく、Flyweightパターンのように見え、動作します。
ガベージコレクションされた言語(GCが確定的でない場合)では、メモリ以外のリソースのクリーンアップをオブジェクトのライフタイムに確実に結び付けることはできません。オブジェクトがいつ削除されるかを述べることはできません。ライフタイムの終了は、完全にガベージコレクタの裁量に任されています。 GCは、オブジェクトが到達可能である間のみそのオブジェクトが存続することを保証します。オブジェクトが到達不能になると、将来のある時点でクリーンアップされる可能性があり、ファイナライザの実行が含まれる可能性があります。
「リソース所有権」の概念は、GC言語には実際には適用されません。 GCシステムはすべてのオブジェクトを所有します。
これらの言語がtry-with-resource + Closeable(Java)、usingステートメント+ IDisposable(C#)、またはステートメント+コンテキストマネージャー(Python)で提供するものは、制御フロー(!=オブジェクト)制御フローがスコープを離れるときに閉じられるリソースを保持します。これらすべてのケースで、これは自動的に挿入されるtry { ... } finally { resource.close(); }
に似ています。リソースを表すオブジェクトの存続期間は、リソースの存続期間とは関係ありません。リソースが閉じられた後もオブジェクトは存続し、リソースが開いている間はオブジェクトに到達できなくなる可能性があります。
ローカル変数の場合、これらのアプローチはRAIIと同等ですが、(デフォルトで実行されるC++デストラクタとは異なり)呼び出しサイトで明示的に使用する必要があります。これを省略した場合、適切なIDEは警告を表示します。
これは、ローカル変数以外の場所から参照されるオブジェクトには機能しません。ここでは、1つ以上の参照があるかどうかは関係ありません。このリソースを保持する別のスレッドを作成することにより、オブジェクト参照によるリソース参照を制御フローによるリソース所有権に変換できますが、スレッドも手動で破棄する必要があるリソースです。
場合によっては、リソースの所有権を呼び出し元の関数に委任することができます。一時オブジェクトが確実にクリーンアップする必要がある(ができない)リソースを参照する代わりに、呼び出し元の関数はクリーンアップする必要があるリソースのセットを保持します。これは、これらのオブジェクトのいずれかの存続期間が関数の存続期間より長くなるまで機能するため、すでに閉じられているリソースを参照します。これは、言語がRustのような所有権追跡を備えていない限り、コンパイラーで検出できません(この場合、このリソース管理の問題には、より良い解決策があります)。
これは唯一の実行可能な解決策として残します:手動でリソースを管理します。これはエラーが発生しやすくなりますが、不可能ではありません。特に、GC言語では所有権について考える必要は珍しいため、既存のコードは所有権の保証について十分に明確ではない場合があります。
他の答えからのたくさんの良い情報。
それでも、明確にするために、あなたが探している可能性のあるパターンは、using
とIDispose
を介してRAIIのような制御フロー構成要素に(いくつかの(オペレーティングシステム)リソースを保持する、より大きな、おそらく参照カウントされる)オブジェクト。
したがって、小さい共有されていない単一の所有者オブジェクトがあり(小さいオブジェクトのIDispose
とusing
制御フロー構成を介して)、大きい共有オブジェクト(おそらくカスタムAcquire
& Release
メソッド)。
(以下に示すAcquire
およびRelease
メソッドは、usingコンストラクトの外部でも使用できますが、try
の暗黙的な安全性なしでusing
に含まれます。)
C#の例
void Test ( MyRefCountedClass myObj )
{
using ( var usingRef = myObj.Acquire () )
{
var item = usingRef.Item;
item.SomeMethod ();
// the `using` automatically invokes Dispose() on usingRef
// which in turn invokes Release() on `myObj.
}
}
interface IReferencable<T> where T: IReferencable<T> {
Reference<T> Acquire ();
void Release();
}
struct Reference<T>: IDisposable where T: IReferencable<T>
{
public readonly T Item;
public Reference(T item) { Item = item; _released = false; }
public void Dispose() { if (! _released ) { _released = true; Item.Release(); } }
private bool _released;
}
class MyRefCountedClass : IReferencable<MyRefCountedClass>
{
private int _refCount = 0;
public Reference<MyRefCountedClass> Acquire ()
{
_refCount++;
return new Reference<MyRefCountedClass>(this);
}
public void Release ()
{
if (--_refCount <= 0)
Dispose();
}
// NOTE that MyRefCountedClass does not have to implement IDisposable, but it can...
// as shown here it doesn't implement the interface
private void Dispose ()
{
if ( _refCount > 0 )
throw new Exception ("Dispose attempted on item in use.");
// release other resources...
}
public int SomeMethod()
{
return 0;
}
}
システム内のオブジェクトの大部分は、一般に次の3つのパターンのいずれかに適合する必要があります。
状態が決して変化せず、状態をカプセル化する手段として参照が保持されるオブジェクト。参照を保持するエンティティは、他のエンティティが同じオブジェクトへの参照を保持するかどうかを知りも気にもしません。
その中のすべての状態の唯一の所有者である単一のエンティティの排他的な制御下にあり、そのオブジェクトを(場合によっては変更可能な)状態をカプセル化する手段として純粋に使用するオブジェクト。
単一のエンティティーによって所有されているが、他のエンティティーは制限された方法での使用が許可されているオブジェクト。オブジェクトの所有者は、状態をカプセル化する手段としてだけでなく、それを共有する他のエンティティとの関係をカプセル化する手段としても使用できます。
そのようなオブジェクトを使用するコードは、最後に残った参照を処理するときに特別なことをする必要がないため、ガベージコレクションの追跡は#1の参照カウントよりもうまく機能します。 #2では参照カウントは必要ありません。これは、オブジェクトの所有者が1人だけであり、オブジェクトが不要になったときにそれを認識するためです。シナリオ3では、他のエンティティがまだ参照を保持しているときにオブジェクトの所有者がオブジェクトを削除すると、問題が発生する可能性があります。そこでも、追跡GCは、死んだオブジェクトへの参照が死んだオブジェクトへの参照として確実に識別可能であり、そのような参照が存在する限り、参照カウントよりも優れている場合があります。
共有可能な所有者なしのオブジェクトにサービスが必要である限り外部リソースを取得および保持させ、サービスが不要になったときに解放する必要がある状況がいくつかあります。たとえば、読み取り専用ファイルの内容をカプセル化するオブジェクトは、お互いの存在を認識したり気にかけたりすることなく、多数のエンティティによって同時に共有および使用できます。ただし、そのような状況はまれです。ほとんどのオブジェクトは、単一の明確な所有者を持つか、所有者なしになります。複数の所有権は可能ですが、ほとんど役に立ちません。
共有所有権はめったに意味をなさない
この答えは少し正接しないかもしれませんが、ユーザー側の立場から所有権を共有するには、いくつのケースが理にかなっているのでしょうか?少なくとも私が働いたドメインでは、実際にはnoneがありました。それ以外の場合、ユーザーが何かを単に削除する必要がないことを意味します。 1つの場所からの時間ですが、リソースが実際にシステムから削除される前に、すべての関連所有者から明示的に削除します。
別のスレッドのように、他のリソースがまだリソースにアクセスしている間にリソースが破壊されるのを防ぐのは、多くの場合、下位レベルのエンジニアリングのアイデアです。多くの場合、ユーザーがソフトウェアから何かを閉じる/削除する/削除することを要求した場合、それはできるだけ早く削除する必要があり(安全に削除できる場合はいつでも)、それが長引いてリソースリークが発生しないようにする必要があります。アプリケーションが実行されています。
例として、ビデオゲームのゲームアセットは、マテリアルライブラリのマテリアルを参照する場合があります。たとえば、あるスレッドでマテリアルライブラリからマテリアルが削除され、別のスレッドがまだゲームアセットによって参照されているマテリアルにアクセスしている場合、ぶら下がりポインタがクラッシュすることは望ましくありません。ただし、これは、ゲームアセットがマテリアルライブラリで参照するマテリアルの所有権を共有することは意味がないという意味ではありません。ユーザーにアセットとマテリアルライブラリの両方からマテリアルを明示的に削除することを強制したくありません。他のスレッドがマテリアルへのアクセスを終了するまで、マテリアルの唯一の賢明な所有者であるマテリアルライブラリからマテリアルが削除されないようにするだけです。
リソースリーク
それでも、ソフトウェアのすべてのコンポーネントにGCを採用した元チームと協力しました。そして、他のスレッドがリソースにアクセスしている間にリソースが破壊されないようにするのに本当に役立ちましたが、代わりにリソースリークのシェアを取得することになりました。
そして、これらは、1時間のセッション後に1キロバイトのメモリがリークするような、開発者だけを混乱させるような些細なリソースリークではありませんでした。これらは壮大なリークであり、多くの場合、アクティブなセッションで数ギガバイトのメモリであり、バグレポートにつながります。たとえば、システムの8つの異なる部分の間でリソースの所有権が参照されている(したがって所有権が共有されている)場合、ユーザーがリソースの削除を要求すると、リソースの削除に失敗するのは1回だけです。漏洩し、おそらく無期限に。
したがって、リークの多いソフトウェアを作成するのが非常に簡単だったので、GCやリファレンスカウントを大規模に適用することはあまり好きではありませんでした。以前は検出が簡単だったぶら下がりポインタクラッシュでしたが、テストのレーダーの下を容易に飛行できる、検出が非常に困難なリソースリークに変わりました。
言語/ライブラリがこれらを提供している場合、弱い参照はこの問題を軽減できますが、混合スキルセットの開発者のチームが適切なときにいつでも弱い参照を一貫して使用できるようにすることは困難であることがわかりました。この問題は、社内チームだけでなく、ソフトウェアのすべてのプラグイン開発者にも関係していました。また、プラグインを原因として追跡するのが困難な方法でオブジェクトへの永続的な参照を保存するだけで、システムがリソースを簡単にリークする可能性があります。そのため、ソフトウェアリソースに起因するバグレポートを大量に入手しました。ソースコードが私たちの管理外にあったプラグインがそれらの高価なリソースへの参照を解放できなかったという理由だけでリークされました。
解決策:据え置き、定期的な削除
したがって、私が個人的なプロジェクトに後で適用した私の解決策は、両方の世界から見つけた一種の最高のものを私に与えましたreferencing=ownership
しかし、リソースの破棄は延期されています。
その結果、ユーザーがリソースの削除を必要とする何かを行うと、APIはリソースを削除するだけで表現されます。
ecs->remove(component);
...ユーザーエンドロジックを非常に簡単な方法でモデル化します。ただし、処理フェーズで他のシステムスレッドが同じコンポーネントに同時にアクセスしている可能性がある場合、リソース(コンポーネント)をすぐに削除できない場合があります。
したがって、これらの処理スレッドは、あちこちで時間を生み出し、ガベージコレクターに似たスレッドがウェイクアップして「stop the world」になり、すべてのリソースを破棄できるようにしますそれらが完了するまで、それらのコンポーネントの処理からスレッドをロックアウトしている間、削除が要求されました。ここで実行する必要のある作業量が一般的に最小限に抑えられ、フレームレートが大幅に削減されないように、これを調整しました。
これは、いくつかの試行錯誤され、十分に文書化された方法であるとは言えませんが、これは私が数年間使用してきたものであり、まったく頭痛やリソースリークはありません。アーキテクチャがこの種の同時実行モデルに適合する可能性がある場合は、GCや参照カウントよりもはるかに手間がかからず、テストのレーダーの下で飛行するこれらのタイプのリソースリークのリスクがないため、このようなアプローチを検討することをお勧めします。
参照カウントまたはGCが有用であるとわかった場所の1つは、永続的なデータ構造です。その場合、それはデータ構造の領域であり、ユーザー側の懸念とはかけ離れており、実際には、各不変コピーが同じ未変更データの所有権を共有する可能性があることは理にかなっています。