16 Nisan 2025
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.
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.
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.
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.
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 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.
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.
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.