İlkay İlknur

Peki Nedir Bu ValueTask?

Ağustos 13, 2020

C# 5.0 ile beraber gelen async/await keywordleri ile beraber .NET içerisinde oldukça kolay bir şekilde asenkron programlama yapabilmekteyiz. Normal durumda asenkron operasyonları yönetmek için oldukça fazla ve karmaşık callback kodu yazmamız gerekirken programlama dili pek çok işlemi bizim yerimize arka planda optimize bir şekilde halletmekte. async/await keywordleri pek çok işlemi arka planda gizlese de bu keywordleri doğru bir şekilde kullanmak da oldukça önemli.

.NET içerisinde asenkron programlama deyince async/await dışında akla gelen şeylerden biri de Task tipi. Task tipi en basit anlatımla asenkron operasyonu temsil eden bir tip. Bu tipi kullanarak asenkron operasyonla ilgili tüm detaylara erişebilmemiz mümkün. Son zamanlarda ise .NET Core içerisinde bazı asenkron operasyonların dönüş değerinin ValueTask olduğunu görmekteyiz. Bu yazımızın konusu da ValueTask kullanımı. Şimdi gelin ilk olarak ValueTask'ın çıkış noktasından başlayalım.

Bugün asenkron bir metot yazmak istediğimizde aşağıdaki şekilde yazabiliyoruz.

public async Task<List<User>> GetAsync()
{
    var users = await GetUsersAsync();
    return ConvertToUsersList(users);
}

Bunun yanında asenkron metotların bazı durumlarda senkron çalışabilmesi gibi bir durum da olabilmekte.

public async Task<int> GetCountAsync()
{
    if(cache==0)
    {
        cache = await CountAsync(); 
    }
    return cache;
}

Yukarıdaki gibi metot yazdığımızda metot sadece cache variable'ı 0 olduğu durumda asenkron çalışmakta. Diğer tüm durumlarda data zaten hazır olduğu için metot senkron olarak çalışıp yanıtı dönmekte. Bu gibi durumlarda metot senkron olarak çalışmasına rağmen arka planda sürekli olarak yeni bir Task tipi üretilmekte. Task tipi bir class olduğu için heapte sürekli olarak Task üretilmesi GC üzerine de ekstra bir yük getirmekte. Özellikle hot path dediğimiz noktalarda bu şekilde büyük oranda senkron çalışan metotların kullanılması arka planda yaratılan Taskların sayısını da oldukça arttıracaktır. Runtime bazı durumlarda arkada yaratılan Taskları cachelese de her durum için cacheleme yapması mümkün olmamakta.

Yukarıda bahsettiğimiz gibi durumların önüne geçmek için .NET Core içerisine ValueTask tipi eklendi. ValueTask bir struct olduğu için senkron çalışma ve başarılı sonuçlanma durumlarında allocationa neden olmamakta. ValueTask kullanmak için metodun dönüş tipini ValueTask olarak değiştirmemiz yeterli.

public async ValueTask<int> GetCountAsync()
{
    if(cache==0)
    {
        cache = await CountAsync(); 
    }
    return cache;
}

ValueTask Kısıtlamaları

ValueTask tipi Task tipinin allocationa neden olmayan bir alternatifi gibi gözüksede Task tipine nazaran önemli kullanım kısıtlamaları bulundurmakta. Öncelikle ValueTask'ı gördüğümüzde tek yapmamız gereken şey sadece bir kez await edip kullanmak ve o ValueTask'a bir daha dokunmamak. Birden fazla kez await etmek, WhenAll, WhenAny gibi combinator metotlar içerisinde kullanmak gibi ihtiyaçlarınız varsa ValueTask bunun için doğru bir tercih değil. Eğer bu gibi kullanımlarda bulunmak isterseniz AsTask() extension metodunu kullanıp Task nesnesi üzerinden bu işlemleri gerçekleştirmeniz gerekmekte.

Doğru ve kaçınmamız gereken kullanımlar şu şekilde.

public async Task<List<User>> GetAsync()
{
    //Dogru
    var count = await GetCountAsync();
 
    //Yanlış
    var valTask = GetCountAsync();
    count = await valTask;
    count = await valTask;
 
    //Yanlış
    var valTask2 = GetCountAsync();
    var task = valTask.AsTask();
    var task2 = valTask.AsTask();
}

Peki artık Task tipini bırakıp doğrudan ValueTask mı kullanmalıyız? Bunun genel olarak cevabı hayır. Çünkü Task tipi şu anda pek çok operasyonu destekleyen bir tip. Asenkron operasyonların çok farklı şekilde yönetiminde kolayca kullanacağımız bir tip. ValueTask tipi çok özel senaryolarda kullanımı avantaj getiren bir tip. Bunun için öncelikle gerekli performans ölçümlerini yaptıktan sonra kullanmakta fayda var. Özellikle ValueTask döndüğümüz metotların nasıl kullanılacağı da oldukça önemli. Desteklenmeyecek şekilde bir kullanımda bulunulacaksa ValueTask yerine Task kullanıp ve bu taskları da cachelenebilecekse cachelemek daha doğru bir çözüm olacaktır. Bu nedenle ölçüm yapıp kazanım getirdiğinden emin olduktan sonra değişimleri yapmakta fayda var.

Bu konu ilginizi çektiyse bu yazıda kaynak olarak kullandığım Understanding the Whys, Whats, and Whens of ValueTask makalesini okuyabilirsiniz.