Selamün Aleyküm Arkadaşlar,
Uzun süredir teknik konularla ilgili çok yazamıyordum. İnsanın zikri ne ise fikri de o olurmuş. O yüzden de dolayı maalesef bu tarz konulara biraz ara verdiğimizi fark ettim. Konu güzel ve hala güncelliğini koruduğunu düşündüğüm bir konu. Sahneye çıkalı çok olsa da kullanılmasıyla ilgili neden ve nerede kullanılıp kullanılmadığını bence birçoğumuzun atladığı bir konu olduğunu düşünüyorum. Bu yönüyle biraz "record" yapıları andırıyor. Daha önceden de araştırmıştım, ancak projede kullanırken karşılaştığım bir uyarı sonrası bu konuda daha da derine inmem gerektiğini hissedince olanlar oldu :) Hazırsak başlayalım.
Konumuz kodu refactor ederken ortaya çıktı. Hobi olarak geliştirdiğim bir projede aşağıdaki gibi bir kod vardı.dbContext.Add(Administrator);
dbContext.Add(Customer);
dbContext.Add(Store);
dbContext.SaveChanges();
Yukarıdaki koda baktığımda ilk olarak bunun senkron olarak eklendiğini hepimiz fark ediyoruzdur. Bunu async olarak yapmanın daha doğru olacağını düşünerek, Add olan metodları AddAsync olarak değiştirdim, SaveChanges metodunu ise SaveChangesAsync olarak değiştirip önüne await anahar kelimesini koydum. Şimdi kodum daha iyi oldu diye düşünmeye başlamıştım.await dbContext.AddAsync(Administrator);
await dbContext.AddAsync(Customer);
await dbContext.AddAsync(Store);
await dbContext.SaveChangesAsync();
Kod yukarıdaki haline bürünmüştü, daha sonra düşündüğümde ise tüm bu işler birbirinden bağımsız bunları Task.WhenAll ile yapsam daha performanslı olmaz mı diye düşündüm. Ne de olsa biri diğerini beklemek zorunda değildi. Kodumu kendimce daha estetik ve perfomanslı hale getirecektim, ama bir de ne göreyim AddAsync metodu "ValueTask" döndüğü için Task.WhenAll ile bu işi yapamıyordum, araştırdım kendimce bunun ValueTask.WhenAll'i (aşağıda değineceğim, cahillik kötü iş) vardır diye ama yokmu. Onun yerine bu taskları AsTask metodunu kullanarak bunları ValueTask'tan Task'a dönüştürebiliyormuşuz. Hmmm öyle mi yapacak bir şey yok diyerek kodu aşağıdaki hale çevirdim.
var admin = dbContext.AddAsync(Administrator).AsTask();
var customer = dbContext.AddAsync(Customer).AsTask();
var store = dbContext.AddAsync(Store).AsTask();
await Task.WhenAll(admin, customer, store);
Bu kod bana gayet mantıklı gelmişti. Bu arada projelerimde "sonarlint" aracını kullanıyorum. Daha önce bu konuyla ilgili şu yazıma da göz atabilirsiniz. Bu sayfaya geldiğimde "Refactor this 'ValueTask' usage to consume it only once" tarzında bir uyar verdiğini gördüm ve AsTask altı çizili idi. Hayrola bu nedir acaba diyerek konuyu araştırmaya başladığımda aslında bu yazının temellerini atmış olduk :)
Konuyu araştırdığımda ValueTask'in sadece bir kez await edilmesi gerektiğini aksi halde problemlere yol açacağı yazılıyordu. Zaten ikinci defa yapıldığında bunun çok da işe yaramyacağını çünkü zaten işlemin bitmiş olacağını gördüm. Kodu şu hale getirerek bu problemi de çözdüğümü düşündüm.
var admin = dbContext.AddAsync(Administrator);
var customer = dbContext.AddAsync(Customer);
var store = dbContext.AddAsync(Store);
await Task.WhenAll(adminTask.AsTask(), customerTask.AsTask(), storeTask.AsTask());
Bu çözümü uygulayarak kodu burada bırakıp gidebilirdim ama gidemedim :) ValueTask'ın bir kez await edilmesi tam olarak ne anlama geliyor, eğer öyle ise Task birden fazla kez mi await edilebilirdi, ama buna neden gerek duyulsun ki? Bu işin mantığı neydi? Nerede hangisini kullanmayalım gibi sorular art arda gelmeye başladı. Araştırdıkça kendimi biraz daha derinlere dalarken buldum. Uzun süredir de böyle keyif alarak bir konuyu araştıramadığımı da itiraf etmem gerekir.
Bildiğiniz üzere ValueTask struct kökenlidir. Task'ın aksine stack bölgesinde oluşturulur (genelde, her zaman değil). Bu gibi konulardan dolayı daha daha hızlı bitmesi gereken işlemlerde kullanılması daha doğru olur. Ben o zamana kadar maalesef biraz ezbere primitive (değer bazlı int, bool gibi) tiplerle kullanıyorken, referans tiplilerde ise Task kullanıyordum, ancak AddAsync metodunun dönüş tipine baktığımda ValueTask döndüğünü görünce gerçekten şaşırdım, bir daha baktım, cidden de ValueTask :)
Ne oluyor acaba diye araştırmaya başladığımda veritabanı işlemlerinin aslında çok da uzun olmadığını bu yüzden ValueTask kullanıldığını gördüm. IO ve dışarıya yapılan isteklerde ise Task kullanmanın daha doğru olacağını gördüm. Gaza gelerek bütün endpoint'lerimi Task'tan ValueTask olarak değiştirmeye karar verdim. Tüm kod derlendi, bir yerde patladı, acaba nedir diye baktığımda "Get" tarafındaki FirstOrDefault metodunun dönüş tipi Task idi. Neden ya, bu da veritabanı işlemi bunun da hızlı olması gerekmez mi diye araştırmaya başladım. Burada da güzel bir aydınlama yaşadım. Her ikisi ne kadar da veritabanı işlemi olsa da veri ekleme işleminin çok daha hızlı olacağı için burada ValueTask kullanmak daha doğru iken FirstOrDefault veya benzer sorgular veriyi sorgulayacağı için burada IO işin içine girer ve bu işlem daha uzun olur. Böylece Task kullanımı tavsiye ediliyor.
Bu açıklamalardan sonra Post endpointlerimi ValueTask olarak ele alıp, Get olanları ise Task olarak değiştirdim. Put ve Delete için ise hala kararsız gibiyim. Eğer Put ve Delete methodları kullanırken önce veriyi çekip daha sonra güncelliyor ise bütün metodu Task yapmak sanki daha mantıklı ancak EF Core 7 ile gelen yeni özellik sayesinde ExecuteUpdate ile ExecuteDelete metodları ile kullanabiliyorsanız bu işlemler hızlansa da Update hala karmaşık işler yapabileceğinden Task öneriliyor, ancak Delete için iki durum da işi kotarır. Ancak birçoğumuzun silme işlemlerinde "soft delete" yaptığını düşünürsek sanki Task kullanımı daha mantıklı gibi duruyor. Zaten iki metodun da dönüş tipini incelediğimizde Task olarak döndüğünü göreceğiz. Her şeye rağmen bunlar genel geçer söylemler. Farklı durumlarda cevaplar elbette değişebilir. Her doğruyu her yerde kullanamayız.
Konudan koptuğumuzun farkındayım ama inanın bana toparlayacağım, yazının başında dediğim gibi bu konu beni baya heyecanlandırdığı için daldan dala farklı konuları anlamaya çalıştım. Şimdi ise yukarıda bahsettiğim bir kere await konusu nedir, biraz da buna bakalım. Bir işlemin yalnızca bir kere await edilmesi durumudur. Yani bir insan bir işlemi neden tekrar kontrol etsin ki dediğinizi duyar gibiyim. Bunun genel manada 2 sebebi var. Biri uzun süren işlemlerde devam eden işlemin ne durumda olduğunu tespit etmek. Diğeri ise paralel işlemlerde birden fazla işlem aynı anda devam ettiği için bu konu önemli hale gelmektedir. Doğal olarak böyle ihtiyaçlarınız olduğunda Task'ı seçmeniz doğru bir davranış olacaktır.
Konu buraya gelmiş iken neden ValueTask.WhenAll yok sorusunu sormuştuk. Bunu beklemek aslında bu işin mantığını anlamadığımın bir göstergesi olabilir. ValueTask paralel kullanıma uygun olarak geliştirilmemiştir. Böyle bir durumda WhenAll gibi paralel işlem yapan bir metod ile çelişmektedir.
Madem ValueTask yapı olarak daha hızlı ve her paralel işlem her zaman doğru olmadığını öğrendiğimize göre (paralel foreach'te de benzer bir kullanım vardı hatırlarsanız), bütün ValueTask olan işlemleri önce Task'a çevirip daha sonra bunları paralel yürütmek çok da mantıklı olmayabilir. O yüzden aslında kodu en son aşağıdaki gibi güncelledim.
await dbContext.AddAsync(Administrator);
await dbContext.AddAsync(Customer);
await dbContext.AddAsync(Store);
await dbContext.SaveChangesAsync();
Şimdi siz bana dönüp deseniz ki bir sürü şey yazmışsın, günün sonunda dönüp dolaşıp aynı şeyi mi yaptın? Haklısınız derim ama tek farkla. Bu sefer ne yaptığımı bilerek yaptım :)
Kodumuzla direkt ilgili olmayan birkaç notu yazdıktan sonra yazıyı sonlandırabiliriz.
- AsTask metodu, ValueTask'ı Task'a çevirir. Çünkü ikinci await kullanımında ValueTask'ların hata fırlatma ihtimali var, ancak ValueTask'ı Task'a çevirmek de ayrı bir maliyet. Bunun yerine böyle durumlarda daha hafif olan Preserve metodunu da kullanabilirsiniz.
- Interface tanımlarken bazen onu implemente eden birden fazla sınıf olabilir. Bunlar senkron ve asenkron olabilir. Eğer onu implemente eden metodlar çoğunlukla asenkron ise Task kullanmak daha mantıklı iken senkron ise ValueTask kullanmak daha mantıklıdır. Çünkü Task'ın maliyetli bir kullanım olduğunu zaten yazı boyunca konuşmuştuk.
- IValueTaskSource diye bir interface var. ValueTask'ın da temelini oluşturur. Uygulama seviyesinde çok kullanılmasa da altyapısal mimari kullanımlarında öne çıkmaktadır. Task ve ValueTask kullanan yapılarda çok ihtiyaç olmaz ama yine de bir kulak aşinalığı olsun istedim.
Özetle bu kadar şeyler yazdık, övdük ettik fakat varsayılan olarak API'lerde dönüş tipi olarak ValueTask mı kullanmalıyız sorusuna cevap hayır. Task'ın Kullanımı basit ve güvenlidir. Aradaki performans farkı da ihmal edilebilir. Tüm bu yazıyı yazdıktan sonra bu cümleyle sonlandırarak umarım sizleri kızdırmamışımdır :)
Yazıyı aşağıdaki hadis-i şerif ve ayetle bitirelim.
👏👏👏
YanıtlaSil