Bellek tarafından yönetilen diller için referans sayma düzeni?


11

Java ve .NET, sizin için belleği yöneten harika çöp toplayıcılara ve harici nesneleri ( Closeable, IDisposable) hızlı bir şekilde serbest bırakmak için uygun kalıplara sahiptir , ancak yalnızca tek bir nesneye aitse. Bazı sistemlerde bir kaynağın iki bileşen tarafından bağımsız olarak tüketilmesi gerekebilir ve yalnızca her iki bileşen de kaynağı serbest bıraktığında serbest bırakılabilir.

Modern C ++ 'da bu sorunu shared_ptr, tüm shared_ptryok edildiğinde kaynağı kararlı bir şekilde serbest bırakacak olan bir ile çözersiniz.

Nesneye yönelik, deterministik olarak çöp toplama sistemlerinde tek bir sahibi olmayan pahalı kaynakları yönetmek ve serbest bırakmak için belgelenmiş, kanıtlanmış modeller var mı?


1
Swift'te de kullanılan Clang'ın Otomatik Referans Sayımı'nı gördünüz mü ?
jscs

1
@JoshCaswell Evet, bu sorunu çözer, ancak çöp toplanan bir alanda çalışıyorum.
C. Ross

8
Referans Sayma olan bir Çöp Toplama stratejisi.
Jörg W Mittag

Yanıtlar:


15

Genel olarak, yönetilmeyen dillerde bile tek bir sahiple kaçınabilirsiniz.

Ancak ilke yönetilen diller için aynıdır. Pahalı kaynağı hemen kapatmak yerine, Close()bir sayacı ( Open()/ Connect()/ vb. Üzerinde artırılır ) 0'a gelinceye kadar kapatırsınız. Büyük olasılıkla Flyweight Pattern gibi görünecek ve hareket edecektir.


Ben de öyle düşünüyordum, ama bunun için belgelenmiş bir model var mı? Flyweight kesinlikle benzer, ancak özellikle tanımlandığı gibi bellek için.
C. Ross

@ C.Ross Bu sonuçlandırıcıların teşvik edildiği bir durum gibi görünüyor. Yönetilmeyen kaynağın çevresinde bir sarmalayıcı sınıfını kullanabilir ve kaynağı serbest bırakmak için bu sınıfa bir sonlandırıcı ekleyebilirsiniz. Ayrıca, uygulamanızı sağlayabilir IDisposable, mümkün olan en kısa sürede kaynağı serbest bırakmak için sayımları tutabilirsiniz. Muhtemelen en iyi şey, çoğu zaman, üçüne de sahip olmaktır, ancak finalizer muhtemelen en kritik kısımdır ve IDisposableuygulama en az kritik olanı.
Panzercrisis

11
@Panzercrisis, kesinleştiricilerin çalışacağı ve özellikle derhal çalışacağı garanti edilmez .
Caleth

@Caleth Sayıların bir şeyin çabukluk kısmına yardımcı olacağını düşünüyordum. Hiç çalışmadıkları sürece, CLR'nin program bitmeden ona ulaşamayacağı anlamına mı geliyor, yoksa diskalifiye olabileceği anlamına mı geliyorsunuz?
Panzercrisis


14

Çöp toplanan bir dilde (GC belirleyici değilse), bellek dışındaki bir kaynağın temizlenmesini bir nesnenin ömrüne güvenli bir şekilde bağlamak mümkün değildir: Bir nesnenin ne zaman silineceğini belirtmek mümkün değildir. Kullanım ömrünün sonu tamamen çöp toplayıcının takdirindedir. GC, yalnızca bir nesneye ulaşılabilirken yaşayacağını garanti eder. Bir nesneye erişilemez hale geldikten sonra, gelecekte kesinleştiricilerin çalıştırılmasını gerektirebilecek bir noktada temizlenebilir.

“Kaynak mülkiyeti” kavramı bir GC dilinde geçerli değildir. GC sistemi tüm nesnelerin sahibidir.

Bu dillerin source-try + Closeable (Java), ifadeler + IDisposable (C #) veya ifadeler + bağlam yöneticileri (Python) ile sundukları, kontrol akışının (! = Nesneler) bir kaynağı tutmasının bir yoludur kontrol akışı bir kapsamdan çıktığında kapatılır. Tüm bu durumlarda, bu otomatik olarak yerleştirilene benzer try { ... } finally { resource.close(); }. Kaynağı temsil eden nesnenin ömrü, kaynağın ömrüyle ilişkili değildir: kaynak kapatıldıktan sonra nesne yaşamaya devam edebilir ve kaynak hala açıkken nesneye erişilemeyebilir.

Yerel değişkenler söz konusu olduğunda, bu yaklaşımlar RAII'ye eşdeğerdir, ancak çağrı sitesinde açıkça kullanılması gerekir (varsayılan olarak çalışacak C ++ yıkıcılarının aksine). İyi bir IDE bu belirtilmediğinde uyarır.

Bu, yerel değişkenler dışındaki konumlardan başvurulan nesneler için çalışmaz. Burada, bir veya daha fazla referans olup olmadığı önemsizdir. Bu kaynağı tutan ayrı bir iş parçacığı oluşturarak, kaynak başvurusunu nesne başvuruları aracılığıyla kaynak akışına denetim akışı aracılığıyla çevirmek mümkündür, ancak iş parçacıkları da el ile atılması gereken kaynaklardır.

Bazı durumlarda, kaynak sahipliğini bir çağrı işlevine devretmek mümkündür. Güvenilir bir şekilde temizlemesi gereken (ancak yapamayan) kaynaklara başvuran geçici nesneler yerine, arama işlevi, temizlenmesi gereken bir dizi kaynağı tutar. Bu, yalnızca bu nesnelerin herhangi birinin ömrü, işlevin ömrünü geçene kadar çalışır ve bu nedenle zaten kapatılmış bir kaynağa başvurur. Dilde Rust benzeri sahiplik izlemesi yoksa bu bir derleyici tarafından algılanamaz (bu durumda, bu kaynak yönetimi sorunu için zaten daha iyi çözümler vardır).

Bu, tek uygulanabilir çözüm olarak kalır: muhtemelen kendiniz sayım referansı uygulayarak manuel kaynak yönetimi. Bu hataya açık, ancak imkansız değil. Özellikle, GC dillerinde sahiplik hakkında düşünmek olağandışı olduğundan, mevcut kod sahiplik garantileri hakkında yeterince açık olmayabilir.


3

Diğer cevaplardan çok iyi bilgi.

Yine de, açık bir şekilde, aradığınız desen, RAII benzeri kontrol akışı yapısı için usingve IDisposebazılarını ( sistem) kaynakları.

Böylece (daha küçük nesneler IDisposeve usingkontrol akışı yapısı aracılığıyla) daha büyük paylaşılan nesneyi (belki de özel Acquireve Releaseyöntemler) bilgilendirebilen, paylaşılmayan küçük tek sahibi nesneler vardır .

( Aşağıda gösterilen Acquireve Releaseyöntemleri daha sonra, kullanılan yapının dışında, ancak tryörtük olanın güvenliği olmadan da kullanılabilir using.)


C # 'da bir örnek

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;
    }
}

Bu C # olması gerekiyorsa (ki buna benziyor), Referans <T> uygulamanız ince bir şekilde yanlıştır. İçin sözleşme IDisposable.Disposemi sesleniyor devletler Disposeaynı nesne üzerinde birden çok kez no-op olmalıdır. Böyle bir model uygulayacak olsaydım, Releasegereksiz hatalardan kaçınmak ve miras yerine yetkiyi kullanmak için özel yapardım (arayüzü kaldırın, SharedDisposablekeyfi Tek Kullanımlık Malzemelerle kullanılabilecek basit bir sınıf sağlayın ), ancak bunlar daha fazla zevk meselesi.
Voo

@Voo, tamam, iyi bir nokta, teşekkürler!
Erik Eidt

1

Bir sistemdeki nesnelerin büyük çoğunluğu genellikle üç modelden birine uymalıdır:

  1. Durumu asla değişmeyecek ve referansları yalnızca devleti kapsülleme aracı olarak tutulan nesneler. Referansları olan varlıklar, başka herhangi bir varlığın aynı nesneye başvuru yapıp yapmadığını bilmez veya önemsemez.

  2. Tek bir varlığın münhasır kontrolü altında olan, buradaki tüm devletin tek sahibi olan ve nesneyi yalnızca (muhtemelen değişebilir) durumu kapsülleme aracı olarak kullanan nesneler.

  3. Tek bir tüzel kişiye ait olan, ancak diğer tüzel kişilerin sınırlı şekillerde kullanmasına izin verilen nesneler. Nesnenin sahibi, onu yalnızca durumu kapsülleme aracı olarak değil, aynı zamanda onu paylaşan diğer varlıklarla da bir ilişkiyi kapsüllemek için kullanabilir.

Çöp toplamayı izlemek, # 1 için referans sayımından daha iyi çalışır, çünkü bu tür nesneleri kullanan kodun, kalan son referansla yapıldığında özel bir şey yapması gerekmez. # 2 için referans sayımı gerekli değildir çünkü nesnelerin tam olarak bir sahibi olacaktır ve artık nesneye ne zaman ihtiyaç duymayacağını bilecektir. Senaryo # 3, bir nesnenin sahibi nesneyi öldürürken, bazı varlıklar hala referansları tutarsa ​​zorluk yaşayabilir; orada bile, bir izleme GC'si, ölü nesnelere yapılan göndermelerin, bu tür referanslar mevcut olduğu sürece, ölü nesnelere yapılan göndermeler olarak güvenilir bir şekilde tanımlanabilmesini sağlamada referans sayımından daha iyi olabilir.

Paylaşılabilir sahipsiz bir nesnenin, hizmetlerine ihtiyaç duyduğu sürece harici kaynakları edinip elinde bulundurması ve hizmetlerine artık gerek duyulmadığında bunları serbest bırakması gereken birkaç durum vardır. Örneğin, salt okunur bir dosyanın içeriğini kapsülleyen bir nesne, herhangi biri birbirinin varlığını bilmek veya önemsemek zorunda kalmadan birçok varlık tarafından aynı anda paylaşılabilir ve kullanılabilir. Ancak bu tür durumlar nadirdir. Çoğu nesnenin ya tek bir temiz sahibi olur, ya da sahipsiz olur. Birden fazla sahiplik mümkündür, ancak nadiren yararlıdır.


0

Paylaşılan Mülkiyet Nadiren Anlamda

Bu cevap biraz teğet olabilir, ancak sormak zorundayım, sahip olmanın paylaşılması için bir kullanıcı sonu açısından kaç vaka mantıklı ? En azından çalıştığım alanlarda neredeyse hiç yoktu çünkü aksi takdirde kullanıcının bir yerden bir kez bir şeyi kaldırması gerekmediği, ancak kaynak gerçekte olmadan önce açıkça ilgili tüm sahiplerden kaldırması gerektiği anlamına gelir. sistemden kaldırıldı.

Başka bir iş parçacığı gibi, başka bir şey buna erişmeye devam ederken kaynakların yok edilmesini önlemek genellikle daha düşük düzeyli bir mühendislik fikridir. Genellikle bir kullanıcı yazılımdan bir şey kapatmak / kaldırmak / silmek istediğinde, mümkün olan en kısa zamanda kaldırılması gerekir (kaldırılması güvenli olduğunda) ve kesinlikle etrafta dolaşmamalı ve kaynak sızıntısına neden olmamalıdır. uygulama çalışıyor.

Örnek olarak, bir video oyunundaki oyun varlığı, malzeme kitaplığındaki bir malzemeye referans verebilir. Başka bir iş parçacığı hala oyun varlığı tarafından başvurulan malzemeye erişirken, malzeme bir iş parçacığında malzeme kitaplığından kaldırılırsa, kesinlikle sarkan bir işaretçi çökmesini istemiyoruz. Ancak bu, oyun varlıklarının referans aldıkları malzemelerin sahipliğini malzeme kütüphanesiyle paylaşmasının bir anlamı olmadığı anlamına gelmez . Kullanıcıyı, malzemeyi hem varlıktan hem de malzeme kitaplığından açıkça kaldırmaya zorlamak istemiyoruz. Sadece malzemelerin tek mantıklı sahibi olan malzeme kütüphanesinden materyalin kaldırılmadığından emin olmak istiyoruz.

Kaynak Sızıntıları

Yine de yazılımdaki tüm bileşenler için GC'yi benimseyen eski bir ekiple çalıştım. Ve bu, diğer iş parçacıkları hala bunlara erişirken hiçbir zaman kaynakların yok edilmemesini sağlamada gerçekten yardımcı olsa da, bunun yerine kaynak sızıntılarından payımızı aldık .

Ve bunlar, bir saatlik bir oturumdan sonra bir kilobayt bellek sızıntısı gibi, yalnızca geliştiricileri üzen türden önemsiz kaynak sızıntıları değildi. Bunlar, aktif bir oturumda genellikle gigabayt bellek gibi epik sızıntılardı ve hata raporlarına yol açtı. Çünkü şimdi bir kaynağın sahipliğine, örneğin 8 farklı bölüm arasında referans verildiğinde (ve dolayısıyla sahiplikte paylaşıldığında), o zaman kaynağın kaldırılmasını isteyen kullanıcıya yanıt olarak kaynağın kaldırılması yalnızca bir tane alır sızdırılmış ve muhtemelen süresiz olarak.

Bu yüzden hiçbir zaman büyük bir GC hayranı ya da herhangi bir geniş çapta uygulanan referans sayımı olmadım çünkü sızdıran yazılım oluşturmayı ne kadar kolay hale getirdiler. Önceden, tespit edilmesi kolay olan sarkan bir işaretçi çökmesi, test radarı altında kolayca uçabilen, algılanması çok zor bir kaynak sızıntısına dönüşür.

Dil / kütüphane bunları sağlarsa zayıf referanslar bu sorunu hafifletebilir, ancak uygun olduğunda zayıf referansları tutarlı bir şekilde kullanabilmek için karışık beceri setleri geliştiricilerinden oluşan bir ekibin bulunmasını zor buldum. Ve bu zorluk sadece iç ekiple değil, yazılımımız için her eklenti geliştiricisiyle de ilgiliydi. Onlar da kolayca bir suçlu olarak eklentiye geri izlemeyi zorlaştıracak şekilde bir nesneye kalıcı bir referans depolayarak sistemin kaynak sızdırmasına neden olabilir, bu yüzden bizim yazılım kaynaklarımızdan kaynaklanan hata raporlarından aslan payımızı aldık çünkü kaynak kodu kontrolümüz dışında olan bir eklenti bu pahalı kaynaklara referans veremedi.

Çözüm: Ertelenmiş, Periyodik Kaldırma

Bu yüzden, daha sonra bana her iki dünyadan bulduğum en iyi şeyi veren kişisel projelerime uyguladığım çözümüm referencing=ownership, kaynakların hala yok edilmesini erteleyen kavramı ortadan kaldırmaktı .

Sonuç olarak, artık kullanıcı bir kaynağın kaldırılmasına neden olan bir şey yaptığında, API yalnızca kaynağı kaldırma açısından ifade edilir:

ecs->remove(component);

... kullanıcı-son mantığını çok basit bir şekilde modeller. Ancak, işlem aşamalarında aynı bileşene aynı anda erişebilecekleri başka sistem iş parçacıkları varsa, kaynak (bileşen) hemen kaldırılamayabilir.

Böylece bu işleme iplikleri daha sonra burada ve orada bir çöp toplayıcısına benzeyen bir ipliğin uyanıp " dünyayı durdurmasına " ve ipliklerin bitene kadar bu bileşenlerin işlenmesinden kilitlenmesi sırasında çıkarılması istenen tüm kaynakları yok etmesine izin veren zaman verir. . Bunu, burada yapılması gereken iş miktarının genellikle minimum olması ve kare hızlarında belirgin bir şekilde kesilmemesi için ayarladım.

Şimdi bunun denenmiş ve test edilmiş ve iyi belgelenmiş bir yöntem olduğunu söyleyemem, ancak birkaç yıldır hiç baş ağrısı ve kaynak sızıntısı olmadan kullandığım bir şey. Mimarinizin bu tür eşzamanlılık modeline uyması mümkün olduğunda, GC veya ref sayımından çok daha az ağır olduğu ve test radarı altında bu tür kaynak sızıntılarını riske atmadığı için böyle yaklaşımları keşfetmenizi öneririm.

Ref sayımı veya GC'nin yararlı olduğunu bulduğum tek yer kalıcı veri yapıları içindir. Bu durumda, veri yapısı bölgesi, kullanıcı sonu endişelerinden çok uzaktır ve orada, her değişmez kopyanın aynı değiştirilmemiş verilerin sahipliğini potansiyel olarak paylaşması mantıklıdır.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.