C# Object Pooling Design Pattern(Object Pooling Tasarım Deseni)
Merhaba,
Bu içeriğimizde OOP temelli geliştirilen uygulamalarda, yaklaşımın esasını teşkil eden nesne(object) kavramının yapısal durumundan yola çıkarak, tekrarlı kullanılan nesnelerin üretim esnasındaki maliyetlerine dair çözüm amaçlı geliştirilmiş Object Pooling Design Pattern üzerine konuşuyor olacağız.
Nedir bu Object Pooling Design Pattern? diye sorarsanız eğer tekrarlı kullanılan nesnelerin üretim ve imha süreçlerinde meydana gelen maliyetlerin minimize edilmesi için üretilen nesnenin bir kaynakta/alanda/havuzda tutulması üzerine geliştirilmiş tasarım desenidir diyebiliriz.
Olayı biraz daha pratiksel bağlamda ele alabilmek için yer yer terminolojik ve programatik bir semantik çizgiden temasta bulunmamızda fayda olacağı kanaatindeyim. Dolayısıyla konuyu C# penceresinden incelediğimiz vakit şöyle bir düşünceyle değerlendirmede bulunabiliriz;
Biliyorsunuz ki herhangi bir T classından bir nesneye ihtiyaç olduğu vakit bunun compilerdan new operatörü aracılığıyla talep edilmesi yeterli olacaktır.
Örneğin;
İşte buradaki işlem neticesinde bir T nesnesi oluşturulmakta lakin bu işlev oldukça maliyetli bir süreç neticesinde gerçekleştirilmektedir. new operatörü gerçekten çok maliyet bir operasyonel ağırlığa sahiptir. O yüzden bol keseden kullanılmaması gerekmekte ve işte tamda bu sebepten dolayı tekrarlı kullanılan/kullanılacak olan nesnelerde new ile yeniden ve yeniden üretimden kaçınılması gerekmektedir. Ayrıca bir nesnenin maliyetinin sadece üretiminden ibaret olacağını düşünmekte eksik bir kanaat olacaktır. Bazen nesneler üretildikleri gibi imha edilirlerken de yüksek maliyet gerektirebilmektedirler.
Şöyle ki…
Aşağıdaki ‘ExampleObject’ isimli sınıfı ele alırsak eğer;
class ExampleObject { int count; public ExampleObject(int l) { //Constructor while (count < l) count++; } ~ExampleObject() { //Destructor while (count > 0) count--; } }
görüldüğü üzere ilgili sınıftan ihtiyaç doğrultusunda yapılacak olan nesne talebi ve ihtiyaç sonrası imha süreçlerinde maliyetli işlemler söz konusudur. Şimdi düşünürsek eğer bu sınıftan
şeklinde her nesne üretiminde bir üretimsel maliyet ortaya çıkacaktır ve ihtiyaç doğrultusunda oluşturulacak olan bu nesnelerde bu maliyet göz ardı edilmek mecburiyetinde kalınacaktır. İşte bunun gibi nesneleri sürekli new ile yaratıp destroy etmekten ziyade bu nesnelerin bir kere oluşturulması ve ardından bir havuzda saklanması, sonra ihtiyaç halinde nesneyi tekrardan yaratmak yerine havuzdan alarak kullanılması ve kullanım bitince tekrardan havuza bırakılması en doğrusu olacaktır… Haliyle bu stratejik yapılanmaya da Object Pooling denmektedir 🙂
Neden Object Pooling stratejisi kullanılmalıdır?
Maliyetli nesnelerin üretimi süreçlerinde ‘creation time’ı mümkün mertebe minimize ederek zamandan kazanç elde etmek ve bütünsel açıdan yazılımın dinamizmini destekleyerek performansı arttırmak için Object Pooling kullanılmalıdır. Tabi ki de new operatöründeki yüksek maliyete bizler bu strateji ile geçici çözüm sunmuş olacak, esasında gerçek sorumluluğun dil geliştiricileri tarafından üstlenilmesi gerekecektir.
Peki, Object Pooling’in Prototype Design Pattern‘dan farkı nedir?
İçeriğimizde sürekli new operatörünün maliyetine vurgu yapmış bulunmaktayız. Lakin esas maliyetin yukarılarda örneği verilen ve constructor’ında ekstra iş yükü barındıran nesneler üzerinde new ile yapılan üretimler neticesinde olduğununda farkında olunduğunun kanaatindeyim. Velhasıl kelam, Prototype deseninde constructor üzerinde iş yükü aşırı derecede fazla olan ve bunun yanında birde parametreli yapıcı ile developer açısından da maliyeti katlayan nesnelere ihtiyaç doğrultusunda üretimsel ağırlığı ortadan kaldırmak için önceden üretilmiş olan bir nesnenin klonlanması ve neticede klonlanmış bu nesne üzerinde yapıcı maliyetinin ortadan kaldırılması amaçlanmaktadır. Object Pooling deseninde ise oluşturulması maliyetli olan nesnelerin bir kereye mahsus üretilip sonraki ihtiyaçlarda yine aynı nesnenin kullanılmasına imkan tanınması esas alınmakta ve yeniden kullanılabilirliğe odaklanılmaktadır. Nihai olarak; prototype tasarımı bir nesne daha üretirken, object pooling tasarımında nesne üretilmeksizinn var olan nesne kullanılmış olacaktır.
Object Pooling Örneklendirmesi
Basit bir object pooling tasarımı için aşağıdaki örneği inceleyebilirsiniz;
class ObjectPool<T> { private readonly ConcurrentBag<T> _objects; private readonly Func<T> _objectGenerator; public ObjectPool(Func<T> objectGenerator) { _objectGenerator = objectGenerator ?? throw new ArgumentException(nameof(objectGenerator)); _objects = new ConcurrentBag<T>(); } public T Get() => _objects.TryTake(out T item) ? item : _objectGenerator(); public void Return(T item) => _objects.Add(item); }
Bir object pooling tasarımında; oluşturulan object havuzunun(ConcurrentBag) yanında, havuzdan nesne almamızı(Get) ve alınan nesneyi daha sonraki ihtiyaçlar doğrultusunda tekrar kullanılabilmesi için havuza geri göndermemizi(Return) sağlayan niteliklerin bulunması gerekmektedir. Tabi burada konuya dair internet ve diğer kaynaklarda Get metodunu Acquire yahut Acquire Object, Return metodunu ise Release yahut Release Object şeklinde farklı terminolojik karşılıklarıyla görebileceğinizi bildirmekte fayda var.
Bizler örneği tatbik etmeye geri döner ve yukarıda tasarlanan ‘ObjectPool’ sınıfını aşağıdaki ‘ExampleObject’ nesnesi ile örneklendirirsek eğer;
class ExampleObject { public ExampleObject(int id) { _id = id; Console.WriteLine($"{nameof(ExampleObject)} nesnesi oluşturulmuştur. Id : {_id}"); } public void Write() => Console.WriteLine($"Id : {_id}"); int _id; }
static void Main(string[] args) { ObjectPool<ExampleObject> objectPool = new ObjectPool<ExampleObject>(() => new ExampleObject(1)); ExampleObject object1 = objectPool.Get(); object1.Write(); objectPool.Return(object1); ExampleObject object2 = objectPool.Get(); object2.Write(); objectPool.Return(object2); }
Görüldüğü üzere ‘ObjectPool’ sınıfı üzerinden oluşturulan ‘objectPool’ nesnesine(yani havuza) bir adet ‘ExampleObject’ nesnesi verilmekte ve bu nesneye ihtiyaç duyulduğu taktirde ‘Get’ fonksiyonuyla havuzdan elde edilerek kullanılmakta, ihtiyaç neticesinde ‘Return’ fonksiyonuyla havuza geri bırakılarak sonraki ihtiyaçlar için elde tutulmaktadır.
Object Pooling; StringBuilder ve DbContext gibi nesnelerde sıklıkla tercih edilen bir stratejidir.
Microsoft.Extensions.ObjectPool Kütüphanesiyle Object Pooling
Object pooling deseni yukarıdaki gibi custom bir şekilde geliştirilebileceği gibi ayrıca Microsoft tarafından hali hazırda sunulan Microsoft.Extensions.ObjectPool kütüphanesindeki classlar eşliğinde de geliştirilebilir.
Bunun için ilk olarak bir object pool yaratabilmek ve hacmini belirleyebilmek için provider kullanılması gerekmektedir. Burada default olarak tasarlanmış ‘DefaultObjectPoolProvider‘ nesnesi tercih edilebilir. Bu provider türü sayesinde oluşturulacak olan pool hacmi Environment.ProcessorCount
değerinin iki katı olarak gelmektedir. Ayrıca nesnelerin yaratılış modellemesinin ve kullanıldıktan sonra havuza iade edilmesinin davranışını belirleyen policy/politika belirlenmesi gerekmektedir. Bunun içinde default tasarlanmış ‘DefaultPooledObjectPolicy‘ nesnesi kullanılabilir. Tabi ki de burada ilgili default sınıflardan türeyen türler oluşturarak ihtiyacınıza dönük custom yapılanmalar geliştirebilir ve kullanabilirsiniz.
Şöyle ki;
class ExampleObject { public ExampleObject() { Console.WriteLine($"{nameof(ExampleObject)} nesnesi oluşturulmuştur. Id : {_id}"); } public ExampleObject(int id) { _id = id; Console.WriteLine($"{nameof(ExampleObject)} nesnesi oluşturulmuştur. Id : {_id}"); } public void Write() => Console.WriteLine($"Id : {_id}"); int _id; }
static void Main(string[] args) { DefaultObjectPoolProvider objectPoolProvider = new DefaultObjectPoolProvider(); ObjectPool<ExampleObject> objectPool = objectPoolProvider.Create(new DefaultPooledObjectPolicy<ExampleObject>());//Constructor parametresiz olmalı. ExampleObject object1 = objectPool.Get(); object1.Write(); objectPool.Return(object1); ExampleObject object2 = objectPool.Get(); object2.Write(); objectPool.Return(object2); }
Asp.NET Core – Dependency Injection İle Object Pooling Kullanımı
Asp.NET Core uygulamalarında Object Pooling tasarımı Dependency Injection ile aşağıdaki gibi kullanılabilmektedir.
public class Startup { . . . public void ConfigureServices(IServiceCollection services) { . . . services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>(); services.AddSingleton(p => { var pool = p.GetService<ObjectPoolProvider>(); return pool.Create<ExampleObject>(); }); . . . } . . . }
Görüldüğü üzere, 11. satırda ‘ObjectPoolProvider’ talebine karşılık ‘DefaultObjectPoolProvider’ nesnesi bildirilmekte ve 12. satırda bir önceki satırda dependency injection servisine eklenen ‘ObjectPoolProvider’ nesnesi servisten talep edilerek elde edilen pool provider’ı üzerinden ‘ExampleObject’ nesnesi barındıran bir ‘ObjectPool<ExampleObject>‘ türünden pool(havuz) oluşturulup servise tekrardan eklenmektedir. Haliyle controller sınıflarında aşağıdaki gibi ‘ObjectPool<ExampleObject>‘ türünden istek yapılması ilgili havuzun elde edilip kullanılabilmesi için yeterli olacaktır.
public class HomeController : Controller { private readonly ObjectPool<ExampleObject> _pool; public HomeController(ObjectPool<ExampleObject> pool) { _pool = pool; } public IActionResult Page1() { var e = _pool.Get(); e.Id = 125; e.Write(); _pool.Return(e); return NotFound(); } public IActionResult Page2() { var e = _pool.Get(); e.Write(); _pool.Return(e); return NotFound(); } }
İşte bu kadar…
Nihai olarak Object Pooling’in, üretimi ve imhası oldukça masraflı olan bir sınıfın üretilen nesnesini ihtiyaç doğrultusunda birden fazla yerde kullanabilmek ve böylece maliyeti minimize edebilmek için tercih edilen ve ilgili nesnenin tek elden yönetilebilmesi için singleton olarak tasarlanan bir tasarım deseni olduğunu görmüş, incelemiş ve irdelemiş olduk 🙂
İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…
Hocam Selamlar,
.NET Core’da Array Pooling konusunu da yazmanızı bekliyorum.