メモリ管理言語の参照カウントパターン?


11

Javaと.NETには、メモリを管理するすばらしいガベージコレクターと、外部オブジェクト(CloseableIDisposable)を迅速に解放する便利なパターンがありますが、それらは単一のオブジェクトによって所有されている場合のみです。一部のシステムでは、リソースは2つのコンポーネントによって個別に消費される必要があり、両方のコンポーネントがリソースを解放するときにのみ解放される場合があります。

最新のC ++ではshared_ptr、を使用してこの問題を解決しますshared_ptr。すべてのが破棄されると、リソースが確定的に解放されます。

オブジェクト指向の非決定論的にガベージコレクションされたシステムに単一の所有者がいない高価なリソースを管理およびリリースするための文書化された実証済みのパターンはありますか?


1
あなたは見たことがありクランの自動参照カウントも使用し、スウィフトには
jsc

1
@JoshCaswellはい、それで問題は解決しますが、ゴミ収集スペースで作業しています。
C.ロス

8
参照カウントガベージコレクション戦略です。
ヨルグWミットタグ

回答:


15

一般に、管理されていない言語であっても、所有者を1人にすることで回避できます。

しかし、原則はマネージ言語でも同じです。上の高価なリソースをすぐに閉じる代わりに、0に達するまでClose()カウンターをデクリメント(Open()/ Connect()/ etcで増分)し、その時点でクローズが実際にクローズを行います。フライウェイトパターンのように見え、動作します。


これも私が考えていたものですが、文書化されたパターンはありますか?フライウェイトは確かに似ていますが、特にメモリの場合は通常定義されています。
C.ロス

@ C.Rossこれは、ファイナライザーが推奨されるケースのようです。アンマネージリソースの周りにラッパークラスを使用し、そのクラスにファイナライザーを追加してリソースを解放できます。また、実装してIDisposable、できるだけ早くリソースを解放するために数を数えるなどすることもできます。おそらく最良のことは、多くの場合、3つすべてを持つことですが、ファイナライザがおそらく最も重要な部分であり、IDisposable実装は最も重大ではありません。
Panzercrisis

11
@Panzercrisis。ただし、ファイナライザの実行は保証されておらず、特に迅速な実行は保証されていません。
カレス

@キャレス私はカウントの事が迅速性の部分に役立つだろうと思っていました。それらがまったく実行されていない限り、プログラムが終了する前にCLRがうまく行かないかもしれないということですか、それとも完全に失格になる可能性があるということですか?
パンツァークライシス


14

GCが確定的でないガベージコレクション言語では、メモリ以外のリソースのクリーンアップをオブジェクトのライフタイムに確実に結び付けることはできません。オブジェクトがいつ削除されるかを述べることはできません。ライフタイムの終了は完全にガベージコレクターの裁量です。GCは、オブジェクトが到達可能な間のみ存続することを保証します。オブジェクトが到達不能になると、将来のある時点でクリーンアップされる可能性があり、ファイナライザの実行が必要になる場合があります。

「リソース所有権」の概念は、GC言語には実際には適用されません。GCシステムはすべてのオブジェクトを所有しています。

これらの言語がtry-with-resource + Closeable(Java)、ステートメント+ IDisposable(C#)を使用して、またはステートメント+コンテキストマネージャー(Python)で提供するものは、制御フロー(!=オブジェクト)がリソースを保持する方法です制御フローがスコープを出ると閉じられます。これらのすべての場合において、これは自動的に挿入されるに似ていますtry { ... } finally { resource.close(); }。リソースを表すオブジェクトのライフタイムは、リソースのライフタイムとは関係ありません。リソースが閉じられた後もオブジェクトは存続し、リソースが開いている間はオブジェクトに到達できなくなる可能性があります。

ローカル変数の場合、これらのアプローチはRAIIと同等ですが、呼び出しサイトで明示的に使用する必要があります(デフォルトで実行されるC ++デストラクタとは異なります)。これを省略すると、優れたIDEが警告します。

これは、ローカル変数以外の場所から参照されるオブジェクトでは機能しません。ここでは、1つ以上の参照があるかどうかは関係ありません。このリソースを保持する別のスレッドを作成することにより、オブジェクト参照によるリソース参照を制御フローによるリソース所有権に変換することができますが、スレッドも手動で破棄する必要があるリソースです。

場合によっては、呼び出し元の関数にリソースの所有権を委任することができます。確実にクリーンアップする必要がある(ただし、クリーンアップできない)リソースを参照する一時オブジェクトの代わりに、呼び出し関数はクリーンアップが必要なリソースのセットを保持します。これは、これらのオブジェクトのいずれかのライフタイムが関数のライフタイムよりも長くなるまで機能するため、すでに閉じられているリソースを参照します。言語にRustのような所有権の追跡がない限り、コンパイラはこれを検出できません(この場合、このリソース管理の問題に対する解決策は既にあります)。

これが唯一の実行可能な解決策として残ります。おそらく、参照カウントを自分で実装することによる手動のリソース管理です。これはエラーを起こしやすいですが、不可能ではありません。特に、GC言語では所有権について考える必要があることは珍しいため、既存のコードでは所有権の保証について十分に明確ではない場合があります。


3

他の回答からの多くの良い情報。

それでも、明示的であることを、あなたが探しているかもしれないパターンでは、RAIIのような制御フロー構造を経由してのために小さな単独で所有するオブジェクトを使用することであるusingIDispose、いくつかの(動作を保持している(おそらく参照カウント、大きい方)オブジェクトと一緒にシステム)リソース。

そのため、小さな共有されていない単一の所有者オブジェクトがあります(小さなオブジェクトIDisposeusing制御フロー構造を介して)、大きな共有オブジェクト(カスタムAcquireReleaseメソッドなど)に通知できます。

(以下に示すAcquireReleaseメソッドは、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;
    }
}

それがC#の場合(これは次のようになります)、Reference <T>の実装は微妙に正しくありません。同じオブジェクトで複数回IDisposable.Dispose呼び出すDisposeことはノーオペレーションでなければならないという状態の契約。このようなパターンを実装するRelease場合、不要なエラーを回避し、継承の代わりに委任を使用するためにプライベートにします(インターフェイスを削除し、SharedDisposable任意のDisposablesで使用できる単純なクラスを提供します)が、それらは好みの問題です。
Voo

@Voo、わかりました、良い点、thx!
エリックエイド

1

システム内の大多数のオブジェクトは、一般に次の3つのパターンのいずれかに適合する必要があります。

  1. 状態が決して変化せず、状態をカプセル化する手段として参照のみが保持されるオブジェクト。参照を保持しているエンティティは、他のエンティティが同じオブジェクトへの参照を保持しているかどうかを知りません。

  2. その中のすべての状態の唯一の所有者である単一のエンティティの排他的制御下にあり、純粋にそのオブジェクトを(おそらく可変)状態をカプセル化する手段として使用するオブジェクト。

  3. 単一のエンティティによって所有されているが、他のエンティティが制限された方法で使用できるオブジェクト。オブジェクトの所有者は、状態をカプセル化する手段としてだけでなく、それを共有する他のエンティティとの関係をカプセル化する手段としても使用できます。

ガベージコレクションの追跡は、#1の参照カウントよりもうまく機能します。そのようなオブジェクトを使用するコードは、最後の残りの参照を行うときに特別なことをする必要がないためです。オブジェクトには1人の所有者しかいないため、参照カウントは必要ありません。また、オブジェクトが不要になった時点がわかります。シナリオ#3は、他のエンティティがまだ参照を保持している間にオブジェクトの所有者がオブジェクトを殺すと、いくつかの困難を引き起こす可能性があります。その場合でも、デッドオブジェクトへの参照がデッドオブジェクトへの参照として存在する限り、デッドオブジェクトへの参照を確実に識別できるようにするために、追跡GCは参照カウントよりも優れている場合があります。

誰かがそのサービスを必要とする限り、共有可能な所有者のないオブジェクトが外部リソースを取得して保持し、サービスが不要になったときにそれらを解放する必要がある状況がいくつかあります。たとえば、読み取り専用ファイルのコンテンツをカプセル化するオブジェクトは、多くのエンティティが同時に共有および使用でき、それらのエンティティはお互いの存在を認識したり、気にしたりする必要はありません。ただし、そのような状況はまれです。ほとんどのオブジェクトは、単一の明確な所有者を持っているか、所有者なしです。複数の所有権は可能ですが、ほとんど役立ちません。


0

共有所有権はほとんど意味がない

この答えはやや外れているかもしれませんが、所有権共有することはユーザーエンドの観点から何の意味があるのでしょうか?少なくとも私が働いていたドメインでは、実際には何もありませんでしたシステムから削除されました。

多くの場合、別のスレッドのように、他の何かがまだリソースにアクセスしている間にリソースが破壊されないようにすることは、低レベルのエンジニアリングアイデアです。多くの場合、ユーザーがソフトウェアから何かを閉じる/削除する/削除するように要求するときは、できるだけ早く削除する必要があります(削除しても安全な場合はいつでも)。アプリケーションが実行されています。

例として、ビデオゲームのゲームアセットは、マテリアルライブラリのマテリアルを参照する場合があります。たとえば、あるスレッドでマテリアルがマテリアルライブラリから削除され、別のスレッドがまだゲームアセットによって参照されているマテリアルにアクセスしている場合、ぶら下がりポインタクラッシュは望ましくありません。しかし、それは、ゲームアセットが参照するマテリアルの所有権をマテリアルライブラリと共有することが意味をなさないということではありません。ユーザーにアセットと素材ライブラリの両方から素材を明示的に削除することを強制したくありません。他のスレッドがマテリアルへのアクセスを完了するまで、マテリアルの唯一の賢明な所有者であるマテリアルライブラリからマテリアルが削除されないようにするだけです。

リソースリーク

しかし、私はソフトウェアのすべてのコンポーネントにGCを採用していた元チームと協力しました。そして、他のスレッドがまだリソースにアクセスしている間にリソースが破壊されないようにするのに本当に役立ちましたが、代わりにリソースリークのシェアを取得することになりました

また、これらは、1時間のセッション後に1キロバイトのメモリリークが発生するなど、開発者だけを混乱させるような些細なリソースリークではありませんでした。これらは重大なリークであり、多くの場合、アクティブなセッションでギガバイトのメモリが使用され、バグ報告につながりました。リソースの所有権が、たとえばシステムの8つの異なる部分の間で参照されている(したがって所有権で共有されている)場合、ユーザーがリソースの削除を要求したことに応答して、リソースの削除に失敗するのは1つだけです漏れ、場合によっては無期限に。

だから、漏れやすいソフトウェアを簡単に作成できるため、GCやリファレンスカウントの大規模なファンではありませんでした。以前はぶら下がりポインタクラッシュでしたが、これは検出が容易で、非常に検出しにくいリソースリークに変わり、テストのレーダーの下で簡単に飛ぶことができます。

言語/ライブラリがこれらを提供する場合、弱参照はこの問題を軽減できますが、混合スキルセットの開発者チームが適切な場合に常に弱参照を一貫して使用できるようにすることは困難であることがわかりました。そして、この難しさは社内チームだけでなく、ソフトウェアのすべてのプラグイン開発者に関係していました。それらもまた、犯人としてプラグインにさかのぼることを困難にする方法でオブジェクトへの永続的な参照を保存するだけで、システムにリソースを簡単にリークさせる可能性があります。ソースコードが制御外にあるプラグインが、これらの高価なリソースへの参照をリリースできなかったためにリークされただけです。

解決策:遅延、定期的な削除

そのため、後に両方の世界で見つけた最高の結果をもたらした個人プロジェクトに後で適用した私のソリューションreferencing=ownershipは、リソースの破壊を延期している概念を排除することでした。

その結果、ユーザーがリソースの削除を必要とする何かをするたびに、APIはリソースを削除するだけで表現されます。

ecs->remove(component);

...ユーザーエンドのロジックを非常に簡単な方法でモデル化します。ただし、同じコンポーネントに同時にアクセスできる処理段階に他のシステムスレッドがある場合、リソース(コンポーネント)はすぐには削除されない可能性があります。

したがって、これらの処理スレッドは、ガベージコレクターに似たスレッドが起動して「世界を停止」し、終了するまでそれらのコンポーネントの処理からスレッドをロックアウトしながら、削除が要求されたすべてのリソースを破棄できるようにする時間をあちこちで生成します。ここで行う必要のある作業の量が一般的に最小限に抑えられ、フレームレートが著しく低下しないように、これを調整しました。

今では、これが試行錯誤され、十分に文書化された方法であるとは言えませんが、これは数年前から使用しており、頭痛もリソースリークもありません。アーキテクチャがこの種の同時実行モデルに適合できる場合は、GCやref-countingよりもはるかに手が軽く、テストのレーダーの下を飛んでいるこれらのタイプのリソースリークのリスクがないため、このようなアプローチを検討することをお勧めします。

ref-countingまたはGCが役立つとわかった場所の1つは、永続的なデータ構造です。その場合、それはデータ構造の領域であり、ユーザー側の懸念とは大きく異なり、実際には、各不変のコピーが同じ変更されていないデータの所有権を共有している可能性があります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.