16 Nisan 2025

Döngüsel Referanslar ve Bellek Sızıntıları: C# Kodunu C++'a Nasıl Taşınır

Kod başarıyla çevrilip derlendikten sonra, özellikle bellek yönetimiyle ilgili, çöp toplayıcısı olan C# ortamına özgü olmayan çalışma zamanı sorunlarıyla sıkça karşılaşırız. Bu makalede, döngüsel referanslar ve erken nesne silinmesi gibi belirli bellek yönetimi sorunlarına derinlemesine inecek ve yaklaşımımızın bunları tespit etmeye ve çözmeye nasıl yardımcı olduğunu göstereceğiz.

Bellek Yönetimi Sorunları

1. Döngüsel Güçlü Referanslar

C#'ta, çöp toplayıcı, erişilemeyen nesne gruplarını tespit edip kaldırarak döngüsel referansları doğru bir şekilde işleyebilir. Ancak C++'ta akıllı işaretçiler referans sayımını kullanır. Eğer iki nesne birbirine güçlü referanslarla (SharedPtr) başvurursa, programın geri kalanından onlara artık harici bir referans olmasa bile referans sayıları asla sıfıra ulaşmaz. Bu durum, bu nesnelerin işgal ettiği kaynaklar asla serbest bırakılmadığı için bir bellek sızıntısına yol açar.

Tipik bir örneği ele alalım:

class Document {
    private Element root;
    public Document()
    {
        root = new Element(this); // Document, Element'e referans verir
    }
}

class Element {
    private Document owner;
    public Element(Document doc)
    {
        owner = doc; // Element, Document'a geri referans verir
    }
}

Bu kod aşağıdaki gibi dönüştürülür:

class Document : public Object {
    SharedPtr<Element> root;
public:
    Document()
    {
        root = MakeObject<Element>(this);
    }
}

class Element {
    SharedPtr<Document> owner; // Güçlü referans
public:
    Element(SharedPtr<Document> doc)
    {
        owner = doc;
    }
}

Burada, Document nesnesi Element'e bir SharedPtr içerir ve Element nesnesi Document'a bir SharedPtr içerir. Bir güçlü referans döngüsü oluşturulur. Başlangıçta Document işaretçisini tutan değişken kapsam dışına çıksa bile, karşılıklı referanslar nedeniyle her iki nesnenin referans sayısı 1 olarak kalacaktır. Nesneler asla silinmez.

Bu, döngüye dahil olan alanlardan birine, örneğin Element.owner alanına CppWeakPtr özniteliği ayarlanarak çözülür. Bu öznitelik, çeviriciye bu alan için güçlü referans sayısını artırmayan bir zayıf referans WeakPtr kullanmasını bildirir.

class Document {
    private Element root;
    public Document()
    {
        root = new Element(this);
    }
}

class Element {
    [CppWeakPtr] private Document owner;
    public Element(Document doc)
    {
        owner = doc;
    }
}

Sonuçta ortaya çıkan C++ kodu:

class Document : public Object {
    SharedPtr<Element> root; // Güçlü referans
public:
    Document()
    {
        root = MakeObject<Element>(this);
    }
}

class Element {
    WeakPtr<Document> owner; // Şimdi bu zayıf bir referans
public:
    Element(SharedPtr<Document> doc)
    {
        owner = doc;
    }
}

Şimdi Element, Document'a zayıf bir referans tutarak döngüyü kırar. Son harici SharedPtr<Document> ortadan kalktığında, Document nesnesi silinir. Bu, Element'in referans sayısını azaltan root alanının (SharedPtr<Element>) silinmesini tetikler. Element'e başka güçlü referans yoksa, o da silinir.

2. Yapılandırma Sırasında Nesne Silinmesi

Bu sorun, bir nesneye kalıcı bir güçlü referans kurulmadan önce, yapılandırılması sırasında başka bir nesneye veya metoda SharedPtr aracılığıyla geçirilirse ortaya çıkar. Bu durumda, yapıcı metot çağrısı sırasında oluşturulan geçici SharedPtr tek referans olabilir. Çağrı tamamlandıktan sonra yok edilirse, referans sayısı sıfıra ulaşır, bu da yıkıcı metodun hemen çağrılmasına ve henüz tam olarak oluşturulmamış nesnenin silinmesine yol açar.

Bir örneği ele alalım:

class Document {
    private Element root;
    public Document()
    {
        root = new Element(this);
    }
    public void Prepare(Element elm)
    {
        ...
    }
}

class Element {
    public Element(Document doc)
    {
        doc.Prepare(this);
    }
}

Çevirici aşağıdaki çıktıyı verir:

class Document : public Object {
    SharedPtr<Element> root;
public:
    Document()
    {
        ThisProtector guard(this); // Erken silinmeye karşı koruma
        root = MakeObject<Element>(this);
    }
    void Prepare(SharedPtr<Element> elm)
    {
        ...
    }
}

class Element {
public:
    Element(SharedPtr<Document> doc)
    {
        ThisProtector guard(this); // Erken silinmeye karşı koruma
        doc->Prepare(this);
    }
}

Document::Prepare metoduna girildiğinde, geçici bir SharedPtr nesnesi oluşturulur ve bu nesne, kendisine kalan güçlü referans olmadığı için tamamlanmamış Element nesnesini silebilir. Önceki makalede gösterildiği gibi, bu sorun Element yapıcı koduna yerel bir ThisProtector guard değişkeni eklenerek çözülür. Çevirici bunu otomatik olarak yapar. guard nesnesinin yapıcısı this için güçlü referans sayısını bir artırır ve yıkıcısı nesne silinmesine neden olmadan tekrar azaltır.

3. Bir Yapıcı Metot İstisna Fırlattığında Nesnenin Çift Silinmesi

Bir nesnenin yapıcı metodunun, bazı alanları oluşturulup başlatıldıktan sonra bir istisna fırlattığı ve bu alanların da yapılandırılmakta olan nesneye geri dönük güçlü referanslar içerebileceği bir durumu düşünün.

class Document {
    private Element root;
    public Document()
    {
        root = new Element(this);
        throw new Exception("Failed to construct Document object");
    }
}

class Element {
    private Document owner;
    public Element(Document doc)
    {
        owner = doc;
    }
}

Dönüşümden sonra şunu elde ederiz:

class Document : public Object {
    SharedPtr<Element> root;
public:
    Document()
    {
        ThisProtector guard(this);
        root = MakeObject<Element>(this);
        throw Exception(u"Document nesnesi oluşturulamadı");
    }
}

class Element {
    SharedPtr<Document> owner;
public:
    Element(SharedPtr<Document> doc)
    {
        ThisProtector guard(this);
        owner = doc;
    }
}

Belgenin yapıcı metodunda istisna fırlatıldıktan ve yürütme yapıcı kodundan çıktıktan sonra, tamamlanmamış Document nesnesinin alanlarının silinmesi de dahil olmak üzere yığın boşaltma başlar. Bu da, silinmekte olan nesneye güçlü bir referans içeren Element::owner alanının silinmesine yol açar. Bu, zaten yıkım sürecinde olan bir nesnenin silinmesiyle sonuçlanır ve çeşitli çalışma zamanı hatalarına neden olur.

Element.owner alanına CppWeakPtr özniteliğini ayarlamak bu sorunu çözer. Ancak, öznitelikler yerleştirilene kadar, öngörülemeyen sonlanmalar nedeniyle bu tür uygulamaların hatalarını ayıklamak zordur. Sorun gidermeyi basitleştirmek için, dahili nesne referans sayacının öbeğe taşındığı ve bir bayrakla desteklendiği özel bir hata ayıklama derleme modu vardır. Bu bayrak yalnızca nesne tam olarak oluşturulduktan sonra – yapıcıdan çıktıktan sonra MakeObject fonksiyonu seviyesinde – ayarlanır. İşaretçi bayrak ayarlanmadan önce yok edilirse, nesne silinmez.

4. Nesne Zincirlerinin Silinmesi

class Node {
    public Node next;
}
class Node : public Object {
public:
    SharedPtr<Node> next;
}

Nesne zincirlerinin silinmesi özyinelemeli olarak yapılır, bu da zincir uzunsa – birkaç bin nesne veya daha fazla – yığın taşmasına neden olabilir. Bu sorun, zinciri yineleme yoluyla silen, bir yıkıcı metoda çevrilen bir sonlandırıcı eklenerek çözülür.

Döngüsel Referansları Bulma

Döngüsel referanslar sorununu düzeltmek basittir – C# koduna bir öznitelik ekleyin. Kötü haber şu ki, ürünü C++ için yayınlamaktan sorumlu geliştirici genellikle hangi spesifik referansın zayıf olması gerektiğini veya bir döngünün var olup olmadığını bilmez.

Döngü aramayı kolaylaştırmak için benzer şekilde çalışan bir dizi araç geliştirdik. Bu araçlar iki dahili mekanizmaya dayanır: global bir nesne kayıt defteri ve bir nesnenin referans alanları hakkındaki bilgilerin çıkarılması.

Global kayıt defteri, o anda var olan nesnelerin bir listesini içerir. System::Object sınıfının yapıcı metodu mevcut nesneye bir referansı bu kayıt defterine yerleştirir ve yıkıcı metot onu kaldırır. Doğal olarak, kayıt defteri yalnızca özel bir hata ayıklama derleme modunda bulunur, böylece dönüştürülmüş kodun yayın modundaki performansını etkilemez.

Bir nesnenin referans alanları hakkındaki bilgiler, System::Object seviyesinde bildirilen GetSharedMembers() sanal fonksiyonu çağrılarak çıkarılabilir. Bu fonksiyon, nesnenin alanlarında tutulan akıllı işaretçilerin ve hedef nesnelerinin tam bir listesini döndürür. Kütüphane kodunda bu fonksiyon manuel olarak yazılırken, üretilen kodda çevirici tarafından gömülür.

Bu mekanizmalar tarafından sağlanan bilgileri işlemenin birkaç yolu vardır. Bunlar arasında geçiş yapmak, uygun çevirici seçenekleri ve/veya önişlemci sabitleri kullanılarak yapılır.

  1. İlgili fonksiyon çağrıldığında, tipler, alanlar ve ilişkiler hakkındaki bilgiler dahil olmak üzere mevcut nesnelerin tam çizgesi bir dosyaya kaydedilir. Bu çizge daha sonra graphviz aracı kullanılarak görselleştirilebilir. Genellikle, sızıntıları kolayca takip etmek için bu dosya her testten sonra oluşturulur.
  2. İlgili fonksiyon çağrıldığında, döngüsel bağımlılıklara sahip olan – yani ilgili tüm referansların güçlü olduğu – mevcut nesnelerin bir çizgesi dosyaya kaydedilir. Böylece, çizge yalnızca ilgili bilgileri içerir. Daha önce analiz edilmiş nesneler, bu fonksiyonun sonraki çağrılarında analizden hariç tutulur. Bu, belirli bir testten tam olarak neyin sızdığını görmeyi çok daha kolaylaştırır.
  3. İlgili fonksiyon çağrıldığında, mevcut izolasyon adaları – tüm referanslarının aynı küme içindeki diğer nesneler tarafından tutulduğu nesne kümeleri – hakkındaki bilgiler konsola yazdırılır. Statik veya yerel değişkenler tarafından referans verilen nesneler bu çıktıya dahil edilmez. Her izolasyon adası türü hakkındaki bilgi, yani tipik bir ada oluşturan sınıflar kümesi, yalnızca bir kez yazdırılır.
  4. SharedPtr sınıfının yıkıcı metodu, yaşam süresini yönettiği nesneden başlayarak nesneler arasındaki referansları dolaşır ve tespit edilen tüm döngüler – başlangıç nesnesine güçlü referansları takip ederek tekrar ulaşılabildiği tüm durumlar – hakkında bilgi yazdırır.

Başka bir yararlı hata ayıklama aracı, bir nesnenin yapıcı metodu MakeObject fonksiyonu tarafından çağrıldıktan sonra, o nesne için güçlü referans sayısının sıfır olup olmadığını kontrol etmektir. Eğer sıfır değilse, bu potansiyel bir sorunu gösterir – bir referans döngüsü, bir istisna fırlatılırsa tanımsız davranış vb.

Özet

C# ve C++ tip sistemleri arasındaki temel uyuşmazlığa rağmen, dönüştürülen kodun orijinaline yakın bir davranışla çalışmasını sağlayan bir akıllı işaretçi sistemi kurmayı başardık. Aynı zamanda, görev tam otomatik modda çözülmedi. Potansiyel sorunların aranmasını önemli ölçüde basitleştiren araçlar oluşturduk.

İlgili Haberler

İlgili videolar

İlgili makaleler