C# ile Asenkron (Asynchronous) Programlamanın Detaylarına İniyoruz

Atalay Kusgoz
TeamDefineX
Published in
8 min readNov 28, 2022

Asenkron programlamayı kullanım her zaman yazılımcıların aklında soru işareti yaratan bir konudur. Birçok yazılımcı “Acaba kodum gerçekten asenkron çalışıyor mu?”, “Asenkron çalıştırdığımda performans daha hızlandı mı?”, “Multithreading mi multitasking mi?” gibi sorulara çok net cevap bulamasa da bu alana bir şekilde dahil oluyor.

Bu konularda birçok yazı olmasına rağmen bu yazıların çoğu, konuya, terimleri açıklamak veya basit bir akış kurgusu üzerinden yaklaşıyor. Biz ise bu sorulara geniş bir perspektiften yaklaşıp karşılaştırmalı örnekler ile cevap vermeye çalışacağız.

Konuya giriş yapmadan önce temel birkaç bilgiyi hatırlayalım.

Paralel Programlama nedir?

Basit olarak ifade etmek gerekirse bir görevi yürütmek için çok çekirdekli işlemcilerin kullanılması ile Thread’ler vasıtasıyla kodun çekirdekler arasında paralel olarak yürütülmesi anlamına gelir. Günümüzdeki birçok teknolojik alet (Bilgisayarlar, Cep telefonları vb.) bu prensiple çalışır.

Asenkron Programlama nedir?

Visual Studio dergisinin tanımına göre Asenkron Programlama, görevin ana uygulama thread’inden ayrı olarak çalıştığı ve çağrılan yeni thread’in tamamlanmasını, başarısızlığını veya ilerlemesini bildirdiği bir paralel programlama aracı olarak tanımlar.

Yazı sonunda paylaşacağım kaynaklar bölümünden bu yazıya ulaşabilirsiniz.

Thread nedir?

Thread, bir process’in (çalışan program, uygulama vs.) aynı anda birden fazla işi yapmasını sağlayan iş parçacığıdır. İşletim sisteminin kaynaklarını kullanır. Bir process’in çalışmaya başlaması ile birlikte bir main thread oluşur ve process içerisinde birden fazla thread oluşturulabiliriz.

Bu yapı, Java’dan node.js’e pek çok programlama dilinde bulunuyor. Microsoft C# 5.0 ise bunun yapılmasını, async ve await keywordleri ile olanak tanıdı.

Peki Async & await ve Task nedir?

· async : İçerisinde asenkron işlem yapılacak metodu belirtir. Asenkron işlem yapılacak metot async keyword’ü ile işaretlenmelidir. Ayrıca metot içerisinde “await” keyword’ü kullanılmalıdır. Sadece “async” kullanımı metodu asenkron yapmaz.

· await : Sadece async ile işaretlenmiş metotlarda kullanılabilir. await keyword’ü Task, Task<T> veya void dönüş tipine sahip metotlarda kullanılabilir. await keyword’ü ile asenkron programlama yapılır.

· Task : Thread yönetiminden daha kolay bir şekilde asenkron olarak çalışabilen kodlar yazabilmemizi sağlar. Yapılması gereken işi taahhüt eder. Bir sonucu var ise bunu geriye döndürebilir.

Kendi yazacağımız metotlarımızda async & await ile asenkron bir yaklaşım yapacaksak Task sınıfını kullanmamız gerekmektedir.

Asenkron programlamada asıl önemli olan I/O ya da CPU bağımlı işlem bir Thread üzerinde yürütüldüğünde sonucun gelinceye kadar thread’in bloklanmayıp diğer isteklerin işletilmesini sağlamasıdır. Bunu context switch tekniği ile arka planda sağlar. Aksi durumda bir sonraki isteği işleyebilmek için önceki isteğin sonuçlanmasını beklemek durumunda kalırız. Yazımızın son kısmında bu farklılığa detaylı bakacağız.

İlk önce Asenkron kodlamanın nasıl yapıldığına hep beraber bakalım.

Yazımızın geri kalanında yukarıda bahsettiğimiz tanımlamaları test edebileceğimiz üç adet örnek bulunmakta.

  • Birinci örneğimizde asenkron metot nasıl yazılır ve main bloğu içerisinde bu metodun await keyword’ü olmadan çağrıldığında nasıl bir sonuç vereceğini ve araya bizim koyacağımız bazı text ifadeler ile işlem sırasının nasıl olacağını gözlemleyeceğiz.
  • İkinci örneğimizde asenkron metotlarımızı bu sefer await keyword’ü kullanarak çağırıp sonuçlarına bakacağız. Burada await keyword’ünün kendinden sonraki kod bloğunu blokladığını ve birden fazla asenkron metot kullanmamız gerektiği durumlarda işlem sonuçlarının nasıl alınması gerektiğinden bahsedeceğiz.
  • Üçüncü örneğimizde ise metot sonuçlarını hem await hem de .Result() olarak almaya çalışılıp bu ikisi arasındaki farkı göstermeye çalışacağız.
  • Son olarak ise Web Api tarafında async — sync metotlara aynı anda birden çok istek göndererek sonuçlarını kıyaslayıp performans testini yapacağız. Çıkacak sonuçlar bize hangi durumlarda async ya da sync metot kullanmayı tercih etmemiz gerektiğiniz gösterecek.

Şimdi örnekler üzerinden incelemelerimize başlayalım.

Örneklerimiz biri çift biri tek sayıları ekrana basıp toplamlarını geri döndüren basit metotlar üzerinden dönecektir. Metotlar içerisindeki kodun asenkron çalışmasını sağlamak için Task.Run ile yeni bir task oluşturuyoruz. Task.Run ile yapmadığımız durumda ve await ile sonucu almadığımız durumda metodu async olarak işaretlesek bile kod asenkron çalışmayacaktır.

Örnek 1:

Asenkron olarak çalıştırdığımız iki metodu arka arkaya main bloğunda await kullanmadan çağırıyoruz. Bu çağrım ile metotların içerisindeki kodun dönüşünü beklemediğini ve iki metodun aynı anda işletildiğini görmekteyiz. Çıktıya baktığımızda ana main bloğunun bloklanmadığını metot işlem sonuçlarından önce “Main Method Bitti” yazısından görmüş oluyoruz.

Örnek 2:

Bu örneğimizde ise metotların geriye döndürdükleri sonuçları almak istiyoruz. Bunun için metotların başına await kelimesini ekledik.

Bu kullanımda main metodumuz await ile çağrım yaptığımız satırı sonuç üretilene kadar bekledi ve daha sonra alttaki satıra geçti. Ancak metotların aynı anda çalışmadığını görmekteyiz.

NOT: Burada dikkat edilmesi gereken yer await kullanımında duraklama başka bir thread üzerinden yürütüldüğü için o arada sunucuda ki başka işlemlere cevap verilebildiği gerçeğidir. Bunun testini makale sonundaki Web API performans testinde değinilecektir. Ayrıca thread’in değiştiğini yukarıda da görmekteyiz.

Metotları aynı anda çalıştırmak istiyorsak çağrımı şu şekilde değiştirmemiz gerekiyor.

Metot sonuçlarını task değişkenine alarak çalışmalarını önceden başlatıyoruz. await Task.WhenAll ile iki metodun da sonucunun dönmesini bekliyoruz ve aşağıdaki sonucu alıyoruz. Bu sayede main metodu içerisinden çağırdığımız iki metot asenkron şekilde çalışmaya başlıyor fakat await satırına geldiğinde bu metotların tamamlanmasını bekliyor. Tamamlandıktan sonra ise await’in devamındaki satırlar işletiliyor.

Örnek 3:

Bu örneğimizde ise iç içe metot çağırımların da asenkron yapıların davranışlarını daha iyi görmek adına iki metot daha eklendi ve eski metotlarımızı bu metotlar tarafından çağırdık.

Yukarıdaki kod çıktısına bakıldığında

  • Main(),CiftlerMainMethod(),TeklerMainMethod() metotlarının aynı zamanda çalıştığı ve birbirini bloklamadığı görülüyor.
  • await CiftSayilar() ve await TekSayilar() kodlarının yazıldığı yerlerdeki metotların yalnızca o metot içerisinde o satırda bloklandığı görülüyor. Birinci maddede belirtildiği gibi main metot içerisinde herhangi bir await kullanımı olmadığı için bir bloklama söz konusu değil.

Şimdi yukarıdaki kodumuzda 4. satırı yorum satırı haline getirip 5.satırı aktif durumu getirelim ve sonucu bakalım. Sonucu await ile değil .Result ile alalım.

Yukarıdaki kod çıktısına bakıldığında

  • var result = CiftSayilar().Result satırında diğer örnekten farklı olarak tüm thread’in bloklandığını görüyoruz. CiftlerMainMethod() metodu bitmeden alttaki satıra geçemiyor. Ne zaman bu metot işlemini bitiriyor, program kaldığı yerden asenkron şekilde çalışmaya devam ediyor. Dikkat ettiğimizde TeklerMainMethod() metodu içerisindeki await satırında dönüş beklenirken main metot içerisindeki “Main Method Bitti” satırı çalışmaya devam etti.

NOT: Burada ekranda sonuçların daha rahat gösterilebilmesi için for döngüleri 10 sayısı kadar döndürüldü. İşlemlerin sonuçlarının daha kesin izlenebilmesi için kendi makinanızda döngüleri 1000 veya üzerini yaparak denemenizi tavsiye ederim.

Yukarıdaki örneklerde asenkron programlama yaparken kullandığımız yapıların davranışlarını detaylı bir şekilde göstermeye çalıştık. Bu yapıların nasıl çalıştığının farkında olarak kodlama yapmamız asıl amacımıza ulaşmamızı sağlayacaktır.

Konuyu kısaca özetlemek gerekirse,

  • Async anahtarı, kullanıldığı metodu tek başına asenkronlaştıran sihirli bir kelime değildir. Bir metodun asenkron olabilmesi için içerisinde yazılan kodun Task.Run ile çalıştırılması veya halihazırda asenkron çalışan bir yapının (Web request vs. gibi) çağrılması gerekmektedir. Async anahtarı bir metodun içerisinde asenkron yapının olduğunu ve bu metodu kullanan yapılarında asenkron olarak ilgili metodu çağırabilmesini sağlamaktadır. Özetle asenkron hiyerarşiyi devam ettirmek içindir.
  • Await anahtarını kullandığımız satırda çağrılan asenkron metodun sonucu beklenir ve sonrasında diğer satırlar işletilmeye devam edilir. Await anahtarının kullanıldığı metot farklı bir metottan çağrılıyor ise ve o metotta await kullanılmıyorsa işlem akışı içerideki metodu beklemeden devam eder. Await kelimesini kullandığımız metodun içerisindeki kod ise burada çağırdığımız asenkron metodun sonucu geldikten sonra işletilir.
  • Async işaretli her metodu await ile çağırmamız gerekmiyor. Eğer ilgili metodun sonucunu onu çağıran metodun içerisinde kullanmayacaksak await yazmadan çağırabilir ve kodun beklemeden işletilmesini sağlayabiliriz.
  • Task olarak dönüş yapan bir metodumuzun sonucunu await yerine .Result() ile almak istersek burada asenkron yapı tamamen kırılıp ana thread bloklanır ve tüm uygulama o satırda bekler. Bu nedenle await’in bir alternatifi olarak .Result() kullanılırken bu detay dikkate alınmalıdır.

Web API Async — Sync Performans Testi

Performans testine başlamadan önce temel birkaç bilgiye değinmek istiyorum.

  • Web uygulamaları gelen her request’i thread pool’daki thread sayısı kadar cevaplayabilirler. Senkron yapılarda thread pool’da kaç tane thread varsa aynı anda o kadar işlem yapılıyorken asenkron uygulamalarda durum bundan biraz daha farklıdır. Çünkü asenkron uygulamalar işlem sonucunun cevabını bekliyorken aynı anda başka bir isteğe de bakabilirler. Thread pool’daki thread sayısını arttırmak bir çözüm gibi gözükse de sunucuya ek maliyet getireceği unutulmamalıdır.
  • I/O’ya bağlı durumlar: Veri tabanına yapılan bir istek, dosya işlemleri ya da bir dış kaynağa (API) sorgu yapılması örnek olarak verilebilir.
  • CPU’ya bağlı durumlar: Kodda CPU tarafından işlenen kodlar. Örnek olarak, bir dizi içerinde döngü veya if-else verilebilir.

Şimdi hangi durumlarda senkron, hangi durumlarda asenkron seçeceğimizi yine örnek üzerinden inceleyelim.

Web API Kodumuz :

Controller’ımızda dört adet endpoint bulunmakta. Bunlar:

  • GetIO = I/O’ya bağlı asenkron işlem
  • GetIOsync = I/O’ya bağlı senkron işlem
  • GetCPU = CPU’ya bağlı asenkron işlem
  • GetCPUsync = CPU’ya bağlı senkron işlem

NOT: CPU’ya bağlı uygulamalarda işlem gecikmesi için döngü sayısını uzun tutmak faydalı olacaktır.

Uygulamamızda kullanılacak thread sayısını kısıtlamak için Startup.cs dosyasına ConfigureServices satır bloğuna aşağıdaki satırı ekliyoruz. Burada önemli olan husus, thread sayısını, işlemcinizdeki çekirdek sayısından az olarak verememenizdir. Kullandığımız işlemci 8 çekirdekli olduğu için 8 üzerinden testimiz gerçekleşecek.

ThreadPool.SetMaxThreads(Environment.ProcessorCount, Environment.ProcessorCount);

Request isteklerini kolay bir şekilde yapabilmek için JMeter uygulamasını kullanacağız. Siz de denemek için https://jmeter.apache.org/ adresinden indirip kurabilirsiniz. JMeter’i aynı anda 105 adet request gönderecek şekilde kurguladım. Bu işlemi şu şekilde gerçekleştirebiliriz.

İlk olarak indirdiğimiz klasör içerisinden /bin klasörüne gidip jmeter(.bat) dosyasını çalıştırıyoruz. Ekrana gelen Test Planı’na sağ tıklayarak Add ->Threads (Users) -> Thread Group’u seçiyoruz.

  • Number of Threads (users): Aynı anda kaç farklı kullanıcı ile istek atılmak istendiğini belirtir.
  • Ramp-up Period (in seconds): Kullanıcıların kaç saniye içerisinde sisteme dahil edilmesi gerektiğini belirtir.
  • Loop Count: Oluşturulan testin kaç kere tekrarlanması istenildiğini belirtir.

Bilgileri doldurduktan sonra Thread Group’a sağ tıklayarak Add ->Sampler -> Http Request’i seçiyoruz. Burada istekte bulanacağımız endpoint’i girip HTTP metodunu seçiyoruz. Son olarak sonuçları gözlemleyebilmek için tekrardan Thread Group’a sağ tıklayarak Add ->Listener-> Summary Report’u seçiyoruz.

Örnek 1 : CPU endpointleri testi

Senkron Metot Sonucuna Baktığımızda:

Asenkron Metot Sonucuna Baktığımızda:

Tüm isteklerin cevaplanma süresi için Max kolonuna bakıyoruz (32 saniye ve 33 saniye olarak gözükmekte) performans olarak pek bir fark olmadığını, hatta asenkron olarak çalışan metodun daha kötü bir performans gösterdiğini görüyoruz.

Örnek 2: I/O endpointleri testi

Burada veritabanı işlemleri konumuz olmadığı için Task.Delay(1000) ile 1 sn işlem süresini bekliyoruz.

Senkron Metot Sonucuna Baktığımızda:

Asenkron Metot Sonucuna Baktığımızda:

Max kolonuna baktığımızda, senkron metodun tüm istekleri cevaplama süresi 14 sn(104 istek için 104/8 = 13 sn + 105. istek 1 sn) iken , asenkron metodun tüm cevapları tamamlama süresi 1 sn olmuş. Bu da ciddi bir performans artışı anlamına geliyor.

Sonuç:

Bu yazımızda Asenkron programlamanın detaylarına inerek örnekler üzerinden inceledik. Hangi durumlarda Async ya da Sync metodunu kullanmamız gerektiğini görmüş olduk. Async metotların CPU’ya bağlı işlemlerde bize pek fazla performans artışı sağlamadığını, I/O’ya bağlı işlemlerde ise gözle görülür bir iyileşme olduğunu test ederek göstermiş olduk.

Okuduğunuz için teşekkür ederim…

Hoşçakalın…

Kaynaklar

https://visualstudiomagazine.com/articles/2011/03/24/wccsp_asynchronous-programming.aspx

https://docs.microsoft.com/en-us/archive/msdn-magazine/2014/october/async-programming-introduction-to-async-await-on-asp-net

--

--