메모리 관리 언어에 대한 참조 카운팅 패턴?


11

Java 및 .NET에는 메모리를 관리하는 멋진 가비지 수집기 및 외부 객체 ( Closeable, IDisposable)를 단일 객체가 소유 한 경우에만 신속하게 해제 할 수있는 편리한 패턴이 있습니다 . 일부 시스템에서는 두 구성 요소가 독립적으로 리소스를 소비해야하며 두 구성 요소가 모두 리소스를 해제 한 경우에만 해제해야합니다.

현대 C ++에서는 모든 문제가 해결 shared_ptr되면 결정적으로 리소스를 해제하는 으로이 문제를 해결합니다 shared_ptr.

객체 지향 비 결정적 가비지 수집 시스템에서 단일 소유자가없는 고가의 리소스를 관리하고 릴리스하기위한 입증되고 입증 된 패턴이 있습니까?


1
Swift 에서도 사용되는 Clang의 자동 참조 카운팅을 보셨습니까 ?
jscs

1
@ JoshCaswell 네, 그러면 문제가 해결되지만 가비지 수집 공간에서 일하고 있습니다.
C. Ross

8
참조 계수 가비지 콜렉션 전략입니다.
Jörg W Mittag

답변:


15

일반적으로 관리되지 않는 언어로 된 단일 소유자를 통해이를 피할 수 있습니다.

그러나 원칙은 관리되는 언어와 동일합니다. 고가의 리소스를 즉시 닫는 대신 0을 눌렀을 때까지 Close()카운터가 감소합니다 ( Open()/ Connect()/ etc로 증가 ). 아마도 플라이급 패턴처럼 보이고 작동 할 것입니다.


이것은 내가 생각한 것이지만 문서화 된 패턴이 있습니까? Flyweight는 확실히 비슷하지만 일반적으로 정의 된 메모리와 관련이 있습니다.
C. Ross

@ C.Ross 이것은 파이널 라이저가 권장되는 경우 인 것 같습니다. 관리되지 않는 리소스 주위에 래퍼 클래스를 사용하여 해당 클래스에 종료자를 추가하여 리소스를 해제 할 수 있습니다. 당신은 또한 그것을 구현하고 IDisposable, 가능한 한 빨리 자원을 릴리스 할 수 있도록 카운트를 유지할 수 있습니다. 아마도 가장 좋은 것은 많은 시간을 가질 것 IDisposable입니다. 가장 중요하지 않습니다.
Panzercrisis

11
@Panzercrisis (파이널 라이저가 실행되지 않을 수 있음을 제외하고, 특히 신속하게 실행되지 않을 수도 있음) .
Caleth

@Caleth 나는 카운트가 신속성에 도움이 될 것이라고 생각했습니다. 그들이 전혀 달리지 않는 한, 프로그램이 끝나기 전에 CLR이 돌아 다니지 못하거나 자격이 완전히 박탈 당할 수 있다는 의미입니까?
Panzercrisis


14

가비지 수집 언어 (GC가 결정적이지 않은 언어)에서는 메모리 이외의 리소스 정리를 개체 수명에 안정적으로 연결할 수 없습니다. 개체가 삭제 될시기를 명시 할 수 없습니다. 수명의 끝은 전적으로 가비지 수집기의 재량에 달려 있습니다. GC는 개체가 도달 가능한 동안 개체가 살도록 보장합니다. 객체에 도달 할 수 없게되면 나중에 언젠가 정리 작업이 필요할 수 있습니다.

“자원 소유권”이라는 개념은 실제로 GC 언어에는 적용되지 않습니다. GC 시스템은 모든 객체를 소유합니다.

이러한 언어가 try-with-resource + Closeable (Java), statement + IDisposable (C #) 또는 statement + context manager (Python)를 사용하여 제공하는 것은 제어 흐름 (! = objects)이 리소스를 보유하는 방법입니다. 제어 흐름이 범위를 벗어나면 닫힙니다. 이 모든 경우에 이것은 자동 삽입과 유사합니다 try { ... } finally { resource.close(); }. 리소스를 나타내는 개체의 수명은 리소스의 수명과 관련이 없습니다. 리소스를 닫은 후에도 개체가 계속 유지 될 수 있으며 리소스가 열려있는 동안 개체에 도달하지 못할 수 있습니다.

로컬 변수의 경우 이러한 접근 방식은 RAII와 동일하지만 기본적으로 실행되는 C ++ 소멸자와 달리 호출 사이트에서 명시 적으로 사용해야합니다. 이것이 생략되면 좋은 IDE가 경고합니다.

로컬 변수 이외의 위치에서 참조되는 객체에는 작동하지 않습니다. 여기서 하나 이상의 참조가 있는지 여부는 관련이 없습니다. 이 자원을 보유하는 별도의 스레드를 작성하여 오브젝트 참조를 통한 자원 참조를 제어 플로우를 통해 자원 소유권으로 변환 할 수 있지만 스레드도 수동으로 버려야하는 자원입니다.

어떤 경우에는 리소스 소유권을 호출 기능에 위임 할 수 있습니다. 호출해야하는 자원을 참조하는 임시 객체 대신 안정적으로 정리할 수는 없지만 호출 함수는 정리해야하는 일련의 자원을 보유합니다. 이것은 이러한 객체의 수명이 함수 수명보다 오래 지속될 때까지만 작동하므로 이미 닫힌 리소스를 참조합니다. 언어에 녹과 유사한 소유권 추적이없는 경우 (이 경우이 자원 관리 문제에 대한 더 나은 솔루션이있는 경우) 컴파일러에서이를 감지 할 수 없습니다.

이는 참조 카운트를 직접 구현하여 수동 리소스 관리가 가능한 유일한 솔루션으로 남습니다. 이것은 오류가 발생하기 쉽지만 불가능하지는 않습니다. 특히 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하는 것은 no-op이어야한다고 명시한 계약입니다 . 그러한 패턴을 구현 Release하려면 불필요한 오류를 피하고 상속 대신 위임을 사용하기 위해 비공개로 만들 것입니다 (인터페이스를 제거하고 SharedDisposable임의의 일회용품과 함께 사용할 수 있는 간단한 클래스를 제공하십시오 ).하지만 더 맛이 있습니다.
Voo

@Voo, 좋아, 좋은 지적, thx!
Erik Eidt

1

시스템에서 대다수의 객체는 일반적으로 다음 세 가지 패턴 중 하나에 맞아야합니다.

  1. 상태가 절대로 변경되지 않고 상태를 캡슐화하는 수단으로 순수하게 참조되는 객체. 참조를 보유한 엔티티는 다른 엔티티가 동일한 오브젝트에 대한 참조를 보유하는지 여부를 알거나 신경 쓰지 않습니다.

  2. 단일 엔티티의 독점적 제어하에있는 오브젝트는 그 안에있는 모든 상태의 단독 소유자이며 오브젝트를 (변경 가능할 수있는) 상태를 캡슐화하는 수단으로 순수하게 사용합니다.

  3. 단일 엔티티가 소유하지만 다른 엔티티가 제한된 방식으로 사용할 수있는 오브젝트. 객체의 소유자는 상태를 캡슐화하는 수단으로 사용할뿐만 아니라 객체를 공유하는 다른 엔티티와의 관계를 캡슐화 할 수도 있습니다.

가비지 수집 추적은 # 1에 대한 참조 횟수보다 더 효과적입니다. 이러한 객체를 사용하는 코드는 마지막 남은 참조로 수행 될 때 특별한 작업을 수행 할 필요가 없기 때문입니다. 객체는 정확히 한 명의 소유자를 가지므로 더 이상 객체가 필요하지 않을 때 알기 때문에 # 2에는 참조 카운팅이 필요하지 않습니다. 시나리오 # 3은 개체의 소유자가 개체를 죽인 반면 다른 개체는 여전히 참조를 보유하면 다소 어려움을 겪을 수 있습니다. 그럼에도 불구하고, 추적 GC는 그러한 참조가 존재하는 한, 죽은 오브젝트에 대한 참조가 죽은 오브젝트에 대한 참조로서 확실하게 식별 될 수 있도록 보장하는 데있어서 참조 카운트보다 낫다.

공유 할 수있는 소유자없는 개체가 다른 사람이 서비스를 필요로하는 한 외부 리소스를 가져 와서 보유해야하고 서비스가 더 이상 필요하지 않은 경우 해제해야하는 상황이 몇 가지 있습니다. 예를 들어, 읽기 전용 파일의 내용을 캡슐화하는 객체는 서로의 존재에 대해 신경 쓰거나 신경 쓸 필요없이 많은 엔티티가 동시에 공유하고 사용할 수 있습니다. 그러나 그러한 상황은 드물다. 대부분의 객체는 하나의 명확한 소유자를 가지거나 소유자가 없습니다. 다중 소유권이 가능하지만 거의 유용하지 않습니다.


0

공유 소유권은 거의 감지되지 않습니다

이 답변은 약간 접하지 않을 수도 있지만 소유권공유 하기 위해 사용자 엔드 관점에서 얼마나 많은 경우가 합리적 입니까? 적어도 내가 일한 도메인에는 실제로 아무것도 없었습니다 . 그렇지 않으면 사용자가 한 곳에서 한 번만 무언가를 제거 할 필요는 없지만 리소스가 실제로되기 전에 모든 관련 소유자로부터 명시 적으로 제거해야 함을 의미합니다 시스템에서 제거되었습니다.

다른 스레드처럼 다른 리소스가 여전히 리소스에 액세스하는 동안 리소스가 손상되는 것을 방지하기 위해 종종 저수준 엔지니어링 아이디어입니다. 종종 사용자가 소프트웨어에서 무언가를 닫거나 제거 / 삭제하도록 요청하는 경우 가능한 빨리 제거해야합니다 (제거하기에 안전 할 때마다). 응용 프로그램이 실행 중입니다.

예를 들어, 비디오 게임의 게임 자산은 재료 라이브러리의 재료를 참조 할 수 있습니다. 예를 들어, 한 스레드에서 재료 라이브러리에서 재료를 제거하고 다른 스레드가 여전히 게임 자산에서 참조하는 재료에 액세스하는 경우 매달려 포인터 충돌이 발생하지 않도록해야합니다. 그러나 이것이 게임 자산이 자신이 참조하는 재질의 소유권 을 재질 라이브러리와 공유 하는 것이 의미가있는 것은 아닙니다 . 우리는 사용자가 자산 및 재료 라이브러리에서 재료를 명시 적으로 제거하도록 강요하지 않습니다. 다른 스레드가 재료에 액세스 할 때까지 재료의 유일한 현명한 소유자 인 재료 라이브러리에서 재료를 제거하지 않기를 원합니다.

자원 유출

그러나 저는 소프트웨어의 모든 구성 요소에 대해 GC를 채택한 전 팀과 함께 작업했습니다. 다른 스레드가 여전히 리소스에 액세스하는 동안 리소스가 파괴되지 않도록하는 데 실제로 도움이되었지만 결국 리소스 누출에 대한 부분을 얻었습니다 .

그리고 이것은 1 시간 동안 세션을 수행 한 후 킬로바이트의 메모리 누수와 같이 개발자 만 화나게하는 사소한 리소스 누출이 아닙니다. 이것은 활동적인 세션에서 종종 기가 바이트의 메모리로 인해 서사시 유출이 발생하여 버그보고를 초래했습니다. 이제 자원의 소유권이 시스템의 8 개의 다른 부분들 사이에서 참조되고 따라서 소유권에서 공유 될 때, 자원의 제거를 요청하는 사용자에 대한 응답으로 자원을 제거하는 데 실패하는 데 단 하나의 시간이 소요되기 때문에 누출 될 가능성이 있으며 무한정 가능합니다.

그래서 나는 누설 소프트웨어를 만드는 것이 얼마나 쉬운 지에 따라 광범위한 규모로 적용되는 GC 또는 레퍼런스 카운팅의 열렬한 팬이 아니 었습니다. 이전에는 감지하기 쉬운 매달린 포인터 충돌이 있었지만 감지하기 어려운 리소스 누수가되어 테스트 레이더에서 쉽게 날 수 있습니다.

언어 / 라이브러리에서 이러한 참조를 제공하는 경우 약한 참조가이 문제를 완화 할 수 있지만 혼합 기술 집합의 개발자 팀이 필요할 때마다 약한 참조를 일관되게 사용할 수 없게 만드는 것이 어렵습니다. 이 어려움은 내부 팀뿐만 아니라 소프트웨어의 모든 단일 플러그인 개발자와 관련이있었습니다. 그것들은 또한 범인으로 플러그인을 추적하는 것을 어렵게하는 방식으로 객체에 대한 지속적인 참조를 저장함으로써 시스템이 쉽게 리소스를 유출하게 할 수 있습니다. 소스 코드가 우리의 통제 범위를 벗어난 플러그인이 고가의 리소스에 대한 참조를 공개하지 못했기 때문에 단순히 유출됩니다.

솔루션 : 지연된 주기적 제거

그래서 나중에 두 가지 세계에서 내가 찾은 최고를 제공하는 개인 프로젝트에 적용한 솔루션은 referencing=ownership여전히 자원 파괴를 지연 시키는 개념을 제거하는 것이 었습니다 .

결과적으로, 이제 사용자가 자원을 제거해야하는 작업을 수행 할 때마다 API는 자원을 제거한다는 관점에서 표현됩니다.

ecs->remove(component);

... 사용자 엔드 로직을 매우 간단하게 모델링합니다. 그러나 처리 단계에 동일한 구성 요소에 동시에 액세스 할 수있는 다른 시스템 스레드가있는 경우 자원 (구성 요소)을 즉시 제거 할 수 없습니다.

따라서 이러한 처리 스레드는 여기저기서 시간을 생성하므로 가비지 수집기와 유사한 스레드가 깨어나 " 세계를 중지 "하고 해당 구성 요소 처리가 완료 될 때까지 스레드를 잠그는 동안 제거 요청 된 모든 리소스를 제거 할 수 있습니다. . 여기서 수행해야 할 작업의 양이 일반적으로 최소화되고 프레임 속도로 눈에 띄게 줄어들지 않도록 이것을 조정했습니다.

이제 이것이 시도되고 테스트되고 잘 문서화 된 방법이라고 말할 수는 없지만 두통이없고 리소스 누수가없는 몇 년 동안 내가 사용해온 것입니다. 아키텍처가 GC 또는 참조 횟수보다 훨씬 덜 무겁고 테스트 레이더에서 이러한 유형의 리소스 누수가 발생할 위험이 없으므로 이러한 종류의 동시성 모델에 적합 할 수있을 때 이와 같은 접근 방식을 탐색하는 것이 좋습니다.

Ref-Counting 또는 GC가 유용한 것으로 밝혀진 곳은 지속적인 데이터 구조입니다. 이 경우 데이터 구조 영역은 사용자와 관련된 문제와는 거리가 멀고 각 변경 불가능한 사본이 수정되지 않은 동일한 데이터의 소유권을 잠재적으로 공유하는 것이 실제로 의미가 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.