Published on

IDisposable Nedir ? C# Bellek Yönetimi (C# Temel Arayüzleri 3)

Authors
  • avatar
    Name
    Alperen Önal
    Twitter

IDisposable'ye Neden İhtiyacım Var ? Unmanaged, Managed Kaynaklar ?

Tüm programlar çalıştırıldıkları zaman bellek, ağ bağlantısı, dosya işleyicisi gibi bir veya birden fazla kaynağı talep eder ve kullanır. Bu tür sistem kaynaklarını kullanırken dikkatli olunmalıdır çünkü aksi taktirde memory leaks(bellek sızıntıları) ve sistem kaynaklarının tükenmesi gibi sorunlar ortaya çıkabilir.

Common Language Runtime, .NET altyapısında programların çalışmasını denetleyen, programın işletim sistemiyle haberleşmesini sağlayan birimdir.

Common Language Runtime kısaca CLR, .NET'de otomatik bellek yönetimi için destek sağlar. Managed kaynağın("new" operatörü kullanılarak ayrılan(allocated) belleğin) programcı tarafından doğrudan serbest bırakılmasına gerek yoktur. Çünkü garbage collector tarafından otomatik olarak serbest bırakılacaktır.

Managed kaynak bir çok sistem kaynağından yalnızca biridir. Unmanaged kaynakların doğrudan programcı tarafından serbest bırakılması gerekmektedir çünkü Garbage Collector bu kaynakları serbest bırakmaz. Kısaca, Unmanaged kaynaklarının yönetimi sorumluluğu programcıya aittir.

CLR, unmanaged kaynakların yönetilmesine kısmi destekleri vardır. Nesnenin kullandığı kaynak Garbage Collector tarafından serbest bırakılmadan önce Garbage Collector'ün çağırdığı ve unmanaged olduğunu belirlediği kaynakların serbest bırakılması için CLR tarafından otomatik olarak Finalizer ile serbest bırakılmasını sağlar.

Madem Finalizer ile unmanaged kaynaklar otomatik serbest bırakabiliyor, Peki neden programcı serbest bırakmak zorunda ?

  • Garbage Collector, bir nesnenin toplamaya uygun olduğunu tespit ettiğinde öncelikle finalizer çağrılır. Ancak bu tespit süreci belirsiz bir zaman olduğu için kaynağın serbest bırakılması için çok geç olabilir. Yani işinizin bittiği halde bir kaynağı gereksiz yere belirsiz bir süre boyunca işgal edebilirsiniz.

  • CRL, bir finalizer çağırdığı zaman ilgili nesnenin kaynağının serbest bırakılması işlemi bir sonraki garbage collect turuna ertelenir(yine zaman belirsizliği :)). Bu, nesnenin kullandığı hafızayı ve atıfta bulunduğu tüm nesneleri uzun bir süre serbest kalmasını engeller.

    Mesela aşağıdaki örnekte, ResourceHolder'den bir nesne oluşturduk diyelim. Daha sonrasında bu nesne ile işimiz bitti. Oluşacak senaryo şu şekilde olacaktır :

    1. GC(Garbage Collector), belirsiz bir süre sonra bu nesnenin artık kullanılmadığını fark eder ve çöp toplama işlemi için bu nesneyi işaretler.

    2. GC, bu nesnenin unmanaged kaynaklar içerdiğini fark eder ve bunları serbest bırakması için finalizer'ı çağıır. GC; çöp toplama işlemini, Finalizer işlemi ile unmanaged kaynaklar serbest bırakılana kadar askıya alır.

    3. GC tarafından çağrılan Finalizer tarafından unmanaged kaynaklar temizlenir ve belirsiz bir süre boyunca bir sonraki çöp toplama turu beklenilir.

    4. Bir sonraki çöp toplama turu geldiğinde managed kaynağı serbest bırakır.

      public class ResourceHolder : IDisposable
    {
      private IntPtr unmanagedResource; // Unmanaged(yönetilmeyen) kaynak.
    
      public ResourceHolder()
      {
          unmanagedResource = ... // Kaynağı ayır
      }
    }
    
    
    

Yukarıdaki örnekte de görebileceğimiz gibi, finalizer kullanarak unmanaged hafızayı serbest bırakmak hiç verimli değil ve çeşitli riskler içeriyor. Bu yüzden IDisposable arayüzü ortaya çıkıyor.


IDisposable Nedir ?

IDisposable, .NET'de kaynak yönetimi için kullanılan bir arayüzdür.

Bir sınıfın IDisposable arayüzünü implement etmesi, o sınıftan oluşturulan bir nesnenin kullanımı bitince o nesneye ayrılan unmanaged ve managed kaynakların serbest bırakılmasını belirtir.

Genellikle unmanaged kaynakları serbest bırakmak için kullanılmaktadır.

unmanaged kaynak, .NET Runtime tarafından yönetilmeyen ve garbage collector tarafından otomatik olarak temizlenmeyen kaynaklardır. Örneğin :

  • Ağ bağlantıları

  • Dosya işleyicileri

  • Sistem API'leri ve işletim sistemi kaynakları vs.

Bazı durumlarda managed kaynaklar, unmanaged kaynaklar gibi "Dispose" yöntemiyle temizlenmek için kullanılabilir.


IDisposable Sınıflarımıza Uygulama(implement) ?

IDisposable'ı sınıflarımıza uygulamak için 'Dispose()' metodunu implement etmeliyiz.

Kullandığımız unmanaged/managed kaynakları Dispose() metodu içerisinde serbest bırakmalıyız.

Örneğin, aşağıdaki gibi bir TcpSocketService isimli bir sınıfımız olsun.

 public class Program
 {

     public static void Main(string[] args)
     {
         IPAddress ipAddress= Dns.GetHostAddresses("alperenonal.com")[0];
         TcpSocketService tcpSocket = new TcpSocketService(ipAddress, 80);
         //diğer işlemler
     }
 }

 public class TcpSocketService:IDisposable
 {
     private TcpClient? _client;
     private readonly IPAddress _address;
     private readonly int _port;
     public TcpSocketService(IPAddress ipAddress, int port)
     {
         _address = ipAddress;
         _port = port;

     }

     public void TcpConnect()
     {
         _client?.Connect(_address, _port);
         //diğer işlemler
     }


     public void Dispose()
     {
         if (_client != null)
         {
            //Kaynakları serbest bırakıyoruz.
             _client.Close(); //bu arada bu metot da Dispose işlemini aynen uygular sadece örnek amaçlı yazdım yani _client.Dipose() ile eşdeğer :)
             _client.Dispose();
             _client = null;

         }

     }
     //diğer metotlar
 }

Şimdi yukarıdaki kodumuzu Disposable pattern'ine uyacak şekilde tekrardan düzenleyelim. Böylece, potansiyel tehlikeleri minimüma düşürürken daha kararlı bir çalışma elde edebiliriz.

Örneğin :

  • Programcının birden fazla kez Dispose metodunu çağırması.

  • Zaten dispose olan bir nesnenin Garbage Collector tarafından tekrardan serbest bırakılmaya çalışılması.

  • Programcının nesneyle işi bittikten sonra Dispose metodunu kullanmayı unutması.

vb. tehlikeleri engellemek istiyoruz.

public class TcpSocketService : IDisposable
{
    private TcpClient? _client;
    private readonly IPAddress _address;
    private readonly int _port;
    private bool _disposed = false; // Dispose metodunun birden fazla kez çalıştırılmasını engeller.

    public TcpSocketService(IPAddress ipAddress, int port)
    {
        _address = ipAddress;
        _port = port;
        _client = new TcpClient(); // TcpClient'i başlatıyoruz
    }

    public void TcpConnect()
    {
        if (_client == null || !_client.Connected)
        {
            _client?.Connect(_address, _port);
            // diğer işlemler
        }
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this); //finalize metodunu boşuna çalıştırma dispose() zaten çalıştı ve kaynaklar serbest bırakıldı.
    }

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed)
            return;

        if (disposing)
        {
            // Yönetilen kaynakları serbest bırak
            _client?.Close(); // Bu çağrı da Dispose işlemini gerçekleştirir
            _client?.Dispose();
        }

        // Yönetilmeyen kaynakları serbest bırak (varsa)

        _client = null; // Referansı null yap

        _disposed = true;
    }

    ~TcpSocketService() //programcı Dispose() metodunu çağırmayı unutursa burası çalışacak. Ve sadece unmanaged(yönetilmeyen) kaynakları serbest bırakacak kalanını GC'ye bırakarak olası risklerin önüne geçecek.
    {
        Dispose(false);
    }
}

(Yukarıdaki kodu Disposable pattern'ine göre ayarladım. Kaynakçadaki URL'lerden araştırmanız tavsiye edilir.)


Unmanaged kaynakların serbest bırakılmasını gösteren bir örnek :


public class UnmanagedResourceHolder : IDisposable
{
    // Unmanaged kaynak
    private IntPtr unmanagedResource;

    // Dispose işaretçisi
    private bool disposed = false;

    public UnmanagedResourceHolder()
    {
        // Unmanaged kaynak tahsis edilir
        unmanagedResource = Marshal.AllocHGlobal(1024); // 1 KB bellek ayır
        Console.WriteLine("Unmanaged resource allocated.");
    }

    public void DoWork()
    {
        if (disposed)
        {
            throw new ObjectDisposedException(nameof(UnmanagedResourceHolder));
        }

        // Unmanaged kaynak ile çalışma yapma
        Console.WriteLine("Working with unmanaged resource.");
    }

    // Dispose metodu, IDisposable'dan gelir
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    // Dispose(bool disposing) metodunu tanımlama
    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                // Yönetilen kaynakları serbest bırak (varsa)
            }

            // Unmanaged kaynakları serbest bırak
            if (unmanagedResource != IntPtr.Zero)
            {
                Marshal.FreeHGlobal(unmanagedResource);
                unmanagedResource = IntPtr.Zero;
                Console.WriteLine("Unmanaged resource released.");
            }

            disposed = true;
        }
    }

    // Finalizer
    ~UnmanagedResourceHolder()
    {
        Dispose(false);
    }
}


Disposable() Metodu Nasıl Kullanılır ?

IDisposable impelement eden sınıfların unmanaged ve managed kaynakları için örnekler yaptık. Peki bu Disposable() metodunu nasıl ve nerede çalıştıracağız ?

("Unmanaged kaynakların serbest bırakılmasını gösteren bir örnek" isimli bir önceki kodumuzu baz alarak aşağıdaki anlatımı yaptım.)

Disposable() metodunu çalıştırmak için 2 yol vardır :

  1. nesne.Disposable() bu şekilde doğrudan çalıştırmak.

    public class Program
    {
        public static void Main(string[] args)
        {
            UnmanagedResourceHolder resourceHolder = new UnmanagedResourceHolder();
    
            resourceHolder.DoWork();
    
            //diğer işlemler
    
            //işimiz bitti artık Dispose edebiliriz.
    
            resourceHolder.Dispose();
    
        }
    }
    
  2. using() sözcüğü ile dolaylı yoldan çalıştırmak.

    public class Program
    {
        public static void Main(string[] args)
        {
            using (UnmanagedResourceHolder resourceHolder = new UnmanagedResourceHolder())
            {
                resourceHolder.DoWork();
                // resourceHolder ile diğer işlemler
            }
    
            // UnmanagedResourceHolder nesnesi burada Dispose edilmiştir
            // Dispose çağrıldıktan sonra unmanagedResource serbest bırakıldı
        }
    }
    

Daha okunabilir olması sebebiyle ve Dispose()'nin çalıştırılmasını unutma gibi tehlikelerin olmaması sebebiyle genellikle 2. yöntem tercih edilmektedir.

Özetle, ilk yöntemle doğrudan çalıştırmak zorundasınız. İkinci yöntemde ise "using" sözcüğünü kullanarak unmanaged kaynak içeren sınıftan nesneyi using parantezleri içerisine yazarak bir örnek oluşturacak daha sonrasında içerisine ihtiyacınız olan kodları yazacaksınız. using() bloklarından çıktığınız zaman otomatik olarak arka planda Dispose() çağrılacaktır.


Umarım açıklayıcı ve yardımcı olmuştur :)

Kaynakça :

  • learn.microsoft.com/dotnet/standard/garbage-collection/implementing-dispose?wt.mc_id=390826

  • learn.microsoft.com/dotnet/api/system.idisposable?view=net-8.0?wt.mc_id=390826

  • learn.microsoft.com/dotnet/standard/design-guidelines/dispose-pattern?redirectedfrom=MSDN#basic-dispose-pattern?wt.mc_id=390826

  • stackoverflow.com/questions/538060/proper-use-of-the-idisposable-interface

  • bytehide.com/blog/idisposable-method-csharp

  • rules.sonarsource.com/csharp/RSPEC-3881/

  • ssw.com.au/rules/when-to-implement-idisposable/