İçeriğe geç
Anasayfa » Coroutines ve Asenkron Programlama

Coroutines ve Asenkron Programlama

Günümüz uygulamalarında platform fark etmeksizin istemcilere hızlı bir akış sunmak ve verilen hizmetin performansını artırmak çok önemlidir. Arka planda yapılan işlemlerin, önyüzde kesintiye sebep olmadan kullanıcıya akıcı bir uygulama sunması gerekir. Asenkron programlama, uygulamanın paralelde birden fazla işi gerçekleştirmesini sağlar. C# dilinde async&await, Java’da rxJava, Spring’de Reactor gibi farklı çözümler sunulurken Android platformunda Kotlin’in sunduğu çözümlerden biri Coroutines mekanizmasıdır.

Coroutine’lere geçmeden önce kısaca senkron ve asenkron programlamaya bakalım. Senkron programlamada kodlar sırayla yukarıdan aşağıya doğru çalışır, bir satırda işlem bitmeden diğer satıra geçilmez. Asenkron programlamada ise bazı işlemler asenkron tanımlanıp arka planda çalıştırılabilir. Kısaca bir thread (iş parçacığı) I/Oişlemini sürdürürken (Klavyeden bilgi girişi, dosyadan okuma yazma gibi işler I/O işlemleridir.) o threadin blok olmadan başka işleri alıp ilerletmesini sağlar ve böylece CPU kullanım performansı yükselir. Aşağıda Şekil 1’de senkron ve asenkron programlama farkı gösterilmektedir. İlk nesil programlama dillerinde, bir web servis isteği yapıldığında onun sonucu gelene kadar UI’da yapılabilecek bağımsız bir işleme izin verilmezdi. Ancak asenkron programlama ile yapabiliyoruz.

Şekil 1. Senkron ve Asenkron Programlama

Coroutine Nedir?

Kotlin’in standart bir parçası değil, kotlinx.coroutines ile gelen JetBrains’in geliştirdiği zengin bir kütüphanedir. Coroutine, thread tarafından yürütülen iş parçalarıdır ya da alt görevlerdir diyebiliriz. Coroutine’lerin en önemli özelliği bir işlemi t anında durdurup daha sonra devam ettirebilmesidir, yani işlemi suspend edebilir.

  • Senkron kod gibi sıralı ve anlaşılır kod geliştirme imkanı sunar.
  • Uygulaması kolaydır.
  • Asenkron işlemlerde yaşanan bazı sorunlara çözüm getirmiştir.
  • Güçlü bir yapıya sahiptir.

Coroutine – Dispatchers Ne İşe Yarar?

Alt görevler arasındaki senkronizasyon yani threadlerin yönetimi dispatcher ile sağlanır. Dispatchers, bir coroutine bloğunun hangi thread üzerinde çalışacağı ve yönetimiyle ilgilenir. Daha detaylı bakacak olursak 4 tip dispatcher bulunur.

  • Dispatchers.Default: CPU işlemleri için kullanılır. Launch , async gibi standart coroutine işlemleri varsayılan olarak bunu kullanır.
  • Dispatchers.IO: Ağ ve disk işlemleri için kullanılır.
  • Dispatchers.Main: Main thread üzerinde çalışır. Tüm UI işlemleri burada yapılır.
  • Dispatchers.Unconfined: Çoğunlukla main thread kullanır ancak thread seçimi kapsama göre yani scope tipine göre değişebileceğinden kullanımı önerilmez.

Suspend vs Block

Coroutine’lerin uygulanmasına geçmeden önce suspend fonksiyonları daha iyi anlayabilmek için suspend ve block arasındaki farka bakmamızda fayda olacaktır.

Şekil 2. Block ve Suspend İşlemleri
  • Blocklama, fonksiyonun çalıştığı threadin, fonksiyon tamamlanana kadar başka bir şey yapamayacağı anlamına gelir. Eğer main thread’de bir block fonksiyon çağrısı yaparsanız kullanıcı ara yüzünü etkili bir şekilde donduracağınız anlamına gelir. Blocklama bitene kadar kullanıcı statik bir ekran görecek ve uygulamayla etkileşimde bulunamayacaktır. Bu durumda beş saniye veya daha uzun süre bırakılırsa uygulama, ANR -Uygulama Yanıt Vermiyor- hatasıyla kilitlenir.
  • Suspend etme, işlemi durdurma ve daha sonra devam ettirmeyi sağlar. Şekil 2’de gösterildiği gibi A işlemi devam ederken B işlemi block edilmeden suspend edilip, A işlemi bitince B işlemi devam ettiriliyor. Suspend fonksiyonlar yalnızca coroutine içinden veya diğer suspend fonksiyonlarından çağrılabilir. Bunun nedeni, içinde çalıştıkları coroutine yordamını askıya almalarıdır. Suspend işlemi, mevcut threadi bir sonuç döndürene kadar başka işler yapmak için serbest bırakır.

Coroutines Temel İşlemlerCoroutines Temel İşlemler

Bir işlemi suspend ederek durdurma ve devam ettirme nasıl gerçekleşiyor? İşlemler arası senkronizasyon nasıl sağlanıyor? Aşağıdaki kod bloğu ile anahtar kelimeleri anlayarak incelemeye başlayalım.

Örnek kod bloğunda, runBlocking ile Coroutine Scope oluşturuluyor.  Coroutine launch edildikten sonra, delay bir saniyeliğine kodu bekletiyor, bu sırada diğer işlem devam ediyor ve Hello yazıyor. Gecikme tamamlanınca ise Android! Yazarak işini bitiriyor.

  • launch: geriye dönüş değeri gerektirmeyen bir coroutine oluşturucusudur. Bağımsız olarak çalışmaya devam eden kodun geri kalanıyla eş zamanlı olarak yeni bir coroutine başlatır. Bu yüzden ilk önce Hello basıldı.
  • delay: özel bir askıya alma işlevidir. Belirli bir süre için coroutine askıya alınır. Bir coroutine askıya alındığında, temeldeki iş parçacığını engellemez, böylece diğer coroutinelerin çalışmasına ve temel iş parçacığını kendi kodları için kullanmasına olanak tanır. Dolayısıyla en çok kullanılan Sleep’ten farkı threadi bloklamıyor oluşudur ve coroutine dışında kullanılamaz, hata verir.
  • runBlocking: senkron çalışan fonksyion ile runBlocking parantezlerinin içindeki kod arasında köprü kuran bir coroutine oluşturucudur. Coroutine tamamlana dek; runBlocking bloğundaki işlemler bitene kadar mevcut thread bloke edilir. Thread pahalı bir kaynak olduğu ve engellenmesi verimsiz olacağı için bu yöntemden kaçınılır. Bu sebeple çok fazla kullanılmaz ya da ana threadde daima en tepede yer alır.

Suspend Fonksiyon Nedir?

Coroutine başlatıcısı (launch) içindeki kodu ayrı bir fonksiyona alıp yeni bir suspend fonksiyonu oluşturalım. Suspend fonksiyon kullanımının diğer fonksiyonlardan farkı yoktur. Peki bize sağladığı özellik nedir? Artık diğer suspend fonksiyonları, delay gibi, bu fonksiyon içinden çağırabiliriz. Eğer fonksiyonumuzu suspend olarak tanımlamazsak buna izin vermeyip editör hata verecektir.

coroutineScope Nedir?

coroutineScope, farklı coroutine oluşturucularından bir tanesidir. coroutineScope bloğunda başlatılan tüm alt öğeler bitene kadar tamamlanmaz ve böylece kendinize özel bir scope oluşturursunuz.

runBlocking ve coroutineScope Farkı Nedir?

İkisi de kendi bloğundaki işlemlerin tamamlanmasını bekler. Temel fark, runBlocking bekleme için mevcut threadi block eder, coroutineScope ise suspend ederek yani askıya alarak mevcut threadi diğer kullanımlar için serbest bırakır. Dolayısıyla runBlocking normal bir fonksiyondur, coroutineScope ise bir askıya alma fonksiyonudur. Bu nedenle coroutineScope suspend fonksiyonlar içinde kullanılabilir, yukarıdaki örnek kod bloğunda olduğu gibi.

coroutineScope ile eş zamanlı ve sıralı işlemler

Bir coroutineScope, birden fazla eşzamanlı işlemi gerçekleştirmek için herhangi bir suspend içinde kullanılabilir. Aşağıda doAndroid() suspend fonksiyonu içinde iki eş zamanlı coroutine başlatalım.

Her iki kod launch bloklarındaki kod eşzamanlı olarak yürütülür; başlangıçtan bir saniye sonra Android 1 ilk olarak yazdırılır ve başlangıçtan iki saniye sonra Android 2 yazdırılır. doAndroid’deki bir coroutineScope ancak her ikisi de tamamlandıktan sonra tamamlanır, dolayısıyla doAndroid fonksiyonu geri döner ve İşlem Tamam mesajını bundan sonra yazdırır.

Job Nedir?

Coroutine başlatma yordamı olan launch, nesneye referans verilerek tamamlanması beklemek için kullanılabilen bir Job nesnesi döndürülür. Böylece senkron kod içinde job.join() çalıştığında buradaki işlem tamamlanması beklenir ve daha sonra senkron kod çalışmaya devam eder.

Async ile değer döndürme

Coroutines Async, değer döndüren asenkron işlemler için kullanılır. Async ile launch arasındaki fark, Job yerine Deferred değerini döndürmesidir. Çalıştırmak için await() fonksiyonunu kullanmamız gerekir. Örnekte iki suspend fonksiyon, getTag() ve getName() fonsksiyonlarından dönen değerleri nasıl çağırdığımıza bakalım.

Coroutines için neden light-weight thread denir?

Threadler işlem süresi baz alınarak karşılaştırıldığında; daha az işlem süresi gerektiren thread için light-weight, uzun süren thread için heavy-wieght denilmiştir. Coroutine’ler aslında thread olmasa da light-weight tabiri kullanılır, bunun sebebi thread gibi çalışıp ancak JVM threadlerine göre daha az bellek tüketmeleridir.

Aşağıdaki kod her biri 5 sn bekleyen ve çok az bellek harcayarak “*” yazdıran 50000 coroutine başlatır. Eğer bunu thread ile yapacak olursak, JDK’ya bağlı olarak yetersiz bellek hatası verir veya aynı anda çalışan çok fazla thread olamayacak şekilde yavaşça threadleri başlatacaktır.

Özetleyecek olursak, coroutine’lerin genel kullanımı ve kavramları hakkında bilgi vermeye çalıştım. Kotlin Coroutines ile daha fazla istek nasıl performanslı bir şekilde karşılanır ve uygulanır konularına yer verdim. Hata yönetimi, oluşturulan coroutine iptal etme gibi detay konulara aşağıda referans aldığım kaynaklardan bakabilirsiniz.

Kaynakça

https://kotlinlang.org/docs/coroutines-guide.html (2023 Ekim)

https://www.kodeco.com/37885995-kotlin-coroutines-tutorial-for-android-getting-started (2023 Ekim)

https://www.koyeb.com/blog/introduction-to-synchronous-and-asynchronous-processing  (2023 Ekim)

https://medium.com/simform-engineering/asynchronous-programming-in-python-9ed85d5ed8a1 (2023 Ekim)

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir