.Net Asenkron(async & await) Programlama

Bu yazıda .Net framework tarafındaki asenkron programlama konusu ele alınacak ve async & await kullanımları incelenecektir.

Engin UNAL
Bilişim Hareketi
7 min readSep 17, 2021

--

async & await konusuna kadar olan kısım oldukça sıkıcı gelebilir. Çok meraklı değilseniz async & await kısmına kadar olan bölümü atlamanızı öneririm, pratikte async ve await kullanımı dışındaki konular karşınıza ender çıkacaktır.

Asenkron programlama elbette geniş ve önemli bir konudur, bu konunun anlaşılması ve sonrasında da doğru uygulanması verimlilik ve performans açısından ciddi farklar oluşmasına neden olacaktır.

C# ‘ta asenkron programlama ile ilgili async ve await kelimeleri ve bunların kullanımlarına geçmeden önce Asenkron Programlama, Multithread Programlama ve Task Parallel Library (TPL) nedir bilinmesi fayda sağlayacaktır. Asenkron programlama için .Net için üç tasarım kalıbı bulunmaktadır, Bunlar :

  • Asynchronous Programming Model (APM)
  • Event-based Asynchronous Pattern (EAP)
  • Task-based Asynchronous Pattern (TAP).

Yazıda Task-based Asynchronous Pattern ve TPL kütüphanesi incelenecektir.

Multithread programlama farklı işlevlerin aynı anda yürütülmesiyle(parallelization) ilgilidir veya daha özeti kodun paralel olarak çalıştırılması diyebiliriz. Başlattığımız thread’lerin aynı anda çalışması durumudur. Bir thread başladığı andan itibaren diğer thread’lerin yaptıkları işlere bağlı olmaksızın paralel olarak çalışır ve sonlanır. Bunu aklımızda canlandırmak için şöyle düşünebiliriz; 8 çekirdekli işlemcide 3 thread başlattığımızı düşünelim ve thread’lerin paralel çalışması demek her bir thread’e bir çekirdeğin atanması gibi düşünülebilir. Böylece işlemcide çalışan 3 thread birbirinden bağımsız yani paralel olarak çalışabilecektir.

Asenkron programlama ise içerisinde Multithread programla kavramını da içeren bir yapıdadır. Temel olarak kodun çalışması sırasında bloklanmaması(non-blocking) gerektiği prensibine dayanır. Bunu da task yapılarıyla sağlar. Uygulamamız birden çok thread içeren bir yapıda olsun ve bu thread’lerin ürettiği sonuçlara göre bazı işlemler yapıyor olsun. İşte bu thread’leri bekleyip gelen sonuçlara göre işlem yapan ve uygulamayı kilitlemeden çalışmasını sağlayan rutinler asenkron programlamanın mantığını oluşturur.
Yani temelde multithread programalama ve asenkron programlama arasındaki fark; Thread yapılacak işlem odaklı çalışır, işlemin sonucunu alana kadar devam eder. Asenkron ise görevlerle ilgilidir. Bir thread’in yürüttüğü işlem bekleme gerektiriyorsa işlemci gücünü boşta bekletmek yerine diğer işlere geçer, arada yapılacak işleri yapar, bekleme gerektiren işleme kaldığı yerden devam eder.

Yukarıda özetlediğim konuları örnek üzerinden inceleyelim. Öncelikle thread örneği açısından en kolayı tek thread ile başlayalım.

Resim 1

Resim 1 ile verilen örnekte, tek thread içeren senkron yapı mevcut. Her görev sırasıyla çalıştırılır, sonraki görevin çalıştırılması için önceki tüm görevlerin çalışmış olması gerekir.

Resim 2

Resim 2'deki örnekte multithread yapısı bulunmaktadır. Her görev ayrı bir thread içerisinde çalışır. Görevlerin tamamlanması süreci birbirinden bağımsızdır.

Resim 3

Resim 3 ile verilen örnekte tek thread içeren asenkron model bulunmakta. Görevler birbiri aralarında serpiştirilmiş durumdalar, bir görevin çalışması için ondan öncekinin tamamlanması beklenmez.

Resim 4

Bir görevin çalışması esnasında bekleme süreleri oluşabilir, bunun çok çeşitli nedenleri olabilir. Örneğin dosya indirme işlemi içeren kod parçası indirme süresinde beklemeye neden olur veya disk işlemleri sırasında bekleme olabilir. Bu bekleme sürelerine blocking denir. Buradaki temel düşünce blocking olduğu durumlarda başka bir görevin işlemleri yapılarak zamanın verimli kullanılmasına dayanır.

Yukarıdaki asenkron örneğinde tek thread bulunuyor fakat .Net dünyasında asenkron programlama için thread pool’dan thread çekiliyor ve çalıştırılan thread’lerin yönetimi sağlanıyor. Bu süreci de Task yapıları ile sağlıyoruz. Task objesi temel olarak üstlendiği işleri thread pool üzerinde asenkron olarak çalıştırır. Yazının devamında TPL ve Task konularına açıklık getirilecektir.

Task Parallel Library (TPL)

TPL, .NET Framework 4 ile gelen ve daha kolay multithread ve paralel programlama yapılmasına imkan sunan bir kod kütüphanesidir. System.Threading ve System.Threading.Tasks namespace altında bir dizi tip ve fonksiyon kümesinden oluşur.
TPL’nin amacı, uygulamalara paralellik ve eşzamanlılık yeteneği eklerken bu süreci daha basitleştirerek geliştiricileri daha üretken hale getirmektir. Daha önceki versiyonlarda da bu tip işlemler tabi ki yapılmaktaydı fakat bu süreçlerin kontrolünün ve koordinasyonunun detayı ve zorluğu ön plandaydı. Bu API ile birlikte daha kolay bir seviyeye getirilmiş oldu.

The Task Parallel Library (TPL), asenkron işlemlerin gerçekleştirilmesi için geliştirilen Task konsepti üzerine kuruludur denilebilir. Task temel olarak thread’e benzer fakat daha üst bir soyutlama sunmaktadır. Thread ile yapmakta zorlandığımız veya yönetmekte sorunlar yaşanan durumlara kolay ve pratik çözümler getiren bir kütüphanedir.

Sunduğu avantajlar;

  • Sistem kaynaklarının daha verimli kullanılabilmesi ve ölçeklenebilirlik.
  • Klasik thread ve paralel programlama api imkanlarına göre çok daha kolay kullanım ve süreç üzerinde daha fazla kontrol sunan api sağlaması. Örneğin bekleme yapma, devam eden işlemi iptal etme, işleme devam etme, hata yakalama, detaylı durum bilgisi alma vb. imkanlar içermesi denilebilir.

Bir örnekle inceleyelim. Aşağıdaki örnekte bir dosya indiren sonra onu işleyen ve işlenmiş halini kaydeden örneği task ile yazdık. Dosya indirme ve işleme gibi işlemleri kod kalabalığı olmasın diye kodlamadım. İçerisinde sadece zaman alacak rutinler bulunmakta. Öncelikle DownloadFile ile dosya indirilmekte devamında ProcessFile ile dosya işlenmekte ve işlenen dosya SaveFile ile kaydedilmekte. Bunlar olurken while döngüsü içerisinde konsola bir yandan * karakteri yazdırılmakta.

Bağlantılı Task Çalıştırma

Yukarıdaki örnekte her task işlemini birbirine zincirleme ContinueWith ile bağladık, biten task sonucunu sonrakinde kullandık. TPL, önceki task bittiğinde sonrakinin otomatik olarak başlamasından ve bu sürecin tanımladığımız şekilde devam etmesinden sorumludur. Tüm task’lerin çalışma sürecinde uygulamamız etkilenmeden konsol ekranına yazdırma işlemlerimizi gerçekleştirebildik. Eğer geliştirdiğimiz kodda sırasıyla değil fakat bir grup olarak bazı task’lerin bitmesinden sonra bir task çalıştırmak istersek Task.WhenAll ile bitmesini istediğimiz task’leri gruplayıp bu işlemler bittikten sonra Task.ContinueWith ile yeni bir task de başlatabiliriz. Bu gibi compose işlemleri gerektiren durumlarda Task.WhenAny ile çoklu task çalıştırma durumlarında task’lerden herhangi birinin tamamlanması durumunda kullanılabilir, Task.Delay ise belirli bir süreden sonra task işleminin bitirileceği durumlarda yardımcı olacaktır.

Task İptal İşlemi

Task’lerimizi iptal etmek istediğimizde ise CancellationToken yapılarını kullanırız. Task’e CancellationToken argümanı geçilip task içerisinde CancellationToken.IsCancellationRequested ile task’in iptal edilme komutunun gönderildiği kontrol edilebilir. Aşağıda ProcessFile için uygulanmış örneği mevcuttur.

Task Hata Ayıklama

Task içerisinden fırlatılan hata doğrudan uygulamanızdaki try/catch bloklarında yakalanmaz bunun yerine task’in son durumunu kontrol etmeniz gerekir. Eğer işlem bitti ise hata var mı kontrolünü (Task.Status) yaparak hatanın detayını alabilirsiniz. Bunun için de Task.Exception nesnesini kullanabilirsiniz.

async & await Kullanımı

Buraya kadar olan bölümde asenkron programlama ve task’lerin zincirleme çalışırken sistemin çalışan diğer elemanlarını bloklamaması konusunda genel bilgiler vermeye çalıştım. async & await kullanmadan TPL’deki Task yapılarıyla asenkron bu işlemleri gerçekleştirdik.

Yazılan kodu incelediğinizde daha şimdiden karmaşıklaşmaya başlayan ve iç içe geçen yapılar olduğu görülebilir. Okunabilirlik ve kullanım kolaylığı açısından C# async ve await anahtar kelimelerini kullanabiliriz.

Öncelikle async ve await kullanmadığmız kodu hatırlayıp DownloadAndProcessFile altında dosya işlemlerimizi toplarlayalım.

DownloadAndProcessFile içerisindeki kısmı async & await kullanarak yazarsak:

Görüldüğü üzere ekstra parantezler ve ContinueWith gibi kelimeler içermeyen async & await ile yazılmış kod çok daha okunabilir ve yazması kolay duruma gelmiş oldu. Yaptığımız en önemli ekleme ise metodların başına await kelimesinin gelmesi oldu. await kelimesi çalıştıracağı task’in işini bitirmesini beklerken diğer işlemlerin asenkron olarak devam etmesini sağlayan özelliği yani non-blocking olarak çalışmayı sağlar.

await sadece Task dönen durumlarda kullanılabilir, C# compiler Task dönmeyen metodlar için await kullanımında hata verecektir. İçerisinde await operatörü kullandığımız metodumuz DownloadAndProcessFile ise async ile başladı. await operatörü async içerisinde kullanılır aksi halde derleme hatası verecektir. Ayrıca await operatörü kullandığımız Task hata fırlattığında try/catch bloğu içinde yakalama imkanı da mevcuttur. Bu da hata ayıklama açısından kolaylık getirmektedir.

Yukarıda anlatılanlara bakılarak async / await kullanımı asenkron işlemleri gerçekleştirme açısından kodlama yükünü azaltmaktadır. Eğer dönüş değeri Task olan bir metodu çağırmak istiyorsanız bulunduğunuz metodun async ile başlaması yeterlidir, devamında await operatörü ile Task dönen bir metodu çağırabilirsiniz.

Eğer Task dönmeyen senkron bir metodu çağırmak durumunda iseniz ve async ile etiketlenmiş bir metod içerisindeyseniz Task.Run kullanabilirsiniz. Compiler tarafında yeni bir thread açılarak kod bu thread içerisinde çalıştırılır. Buradaki önemli noktalardan bir de Task.Run, Task döneceğinden await ile kullanılabilir.

Task.Run örneği:

Task.Run kullanımlarının CPU bağlantılı işlerde kullanılması önerilmektedir. CPU yapıları birden çok çekirdek içerdiğinden işlemci gücü gerektiren kodlamalarda Task.Run ile yeni bir thread açarak çekirdek işlem gücünden daha verimli faydalanabilirsiniz. Örneğin uygulamanızdaki main thread çok yoğun işlem yapıyor olabilir ve işlemci gücü gerektiren kodlamaları Task.Run ile yeni bir thread üzerinde çalıştırdığınızda açılan bu thread farklı bir çekirdek üzerinde çalışacaktır(veya hangi çekirdek düşük işlemde ise onda çalışacaktır) bu nedenle farklı çekirdek üzerinde çalışmanın getirdiği performans artışı elde edilmiş olacaktır.

Eğer dosya işlemleri, ağ işlemleri gibi I/O operasyonları yapıyorsanız Task.Run önerilmez. Bunun nedeni ise I/O işlemlerinde uzun süreli beklemeler olması ve bu durumun işlemci çekirdeğinin bir süre bekleme durumuna düşmesidir. Sistem kaynaklarının verimsiz kullanımına neden olacak bu tip durumlara düşmemek için asenkron metodlar kullanmalısınız.

Buna ek olarak Task.Run kullanımlarını çok gerekmediği durumlarda iç içe kullanmamaya dikkat edilmelidir. Genelde önerilen kullanım, UI koduna doğrudan bağlı veya event handler içerisinde olacak şekildedir.

Task.FromResult ve Task.FromException konularına da kısaca değinmek gerekirse. Task.FromResult genellikle iki sebeple kullanılır. Asenkron bir interface implemente ettiğiniz bir metodda senkron bir sonuç döndürmeniz gerekiyorsa veya test kodunuzda mocking kullanıyorsanız. Hızlıca bir kod örneği ile daha net görelim. Aşağıdaki örnekte verilen bir dizindeki dosyaların toplam boyutunu döndürmek için yazılan kodun bir bölümü bulunmakta. Eğer dizinde hiç dosya yoksa Task.FromResult dönülerek işlem sonlandılır.

Görüldüğü üzere dönüş değeri Task<long > olarak verilen Task için senkron bir işlem gerçekleştirip bu işlemin sonucunu dönmemiz gerektiğinde TaskResult kullanarak dönebiliriz. Çalışma mantığı özetle Task.FromResult ile bir task yaratılır ve completed status alarak ve dönüş değeri olarak atanan değerle birlikte döner. Aynı mantıkla Task.FromException için de benzer şeyler söyleyebiliriz. Verilen bir exception ile Task’i tamamlar ve döndürür. Böylece Task durumu faulted olarak döner.

Yukarıdaki iki kod örneği için Microsoft linki buradadır.

Kompleks mimariler veya daha detaylı kullanımlar için derinlemesine çok detayı bulunan bu konuya özet niteliğindeki yazımın sonuna geldik. Okuduğunuz için teşekkürler.

Engin Ünal

--

--