loading...

Kotlin Coroutine — Asenkron ve Paralel Programlama

canerpatir profile image Caner Patır Originally published at Medium on ・7 min read

kotlin coroutine

Kotlin Coroutine — Asenkron ve Paralel Programlama

Asenkron ve paralel programlama farklı teknoloji ekosistemlerinde farklı anlamlar ifade ettiği için yazıya başlamadan önce bu kavramlara açıklık getirmek istiyorum.

Paralel programlama (Parallelism)

Bir görevin paralel olarak işlenebilecek daha küçük alt görevlere ayrılarak birden fazla CPU’da, threadler vasıtası ile, paralel olarak yürütülmesi anlamına gelir. Paralel işlemler gereken veri ve kaynak bakımından biribirinden bağımsızdır ve çoğunlukla senkronizasyona ihtiyaç duymazlar. Örnek verecek olursak… bir veri kaynağından (DB, filesystem vs.) idendifier’larını bildiğimiz bir veri seti okuyacaksak, bunları belli parçalara bölüp parallel bir şekilde toplayabiliriz.

Asenkron programlama (Concurrency)

Bir uygulamanın aynı anda (concurrency) birden fazla görevde ilerleme kaydettiği anlamına gelir. Ancak bu işler paralel olarak farklı thread lerde işlenmeyebilir. Uzun süren IO görevlerinin CPU’yu bloklamamsı amacıyla kullanılır (non-blocking IO). Bir threadde IO işlemi sürerken o threadin bloklanmayarak, başka görevlere tahsis edilmesi ve CPU’nun yüksek utilizasyon ile kullanılması esasına dayalı bir görevdir. Bunu context switch teknikleri kullanarak sağlar. Bu yöntemin uygulanmasındaki zorluk alt görevler arasındaki senkronizasyonu sağlamaktır. Modern programlama dilleri senkronizasyon problemine derleyici seviyesinde çözümler üretmiştir.

Kotlin programlama dili bu yöntemi sağlayabilmek için coroutine denen bir yapı sunar. Coroutine’ler alt görevleri temsil eder ve dispatcher denen bir yapı vasıtası ile görevler arasındaki senkronizasyon sağlanır. . Özetle, dispatcher threadlerin görevler arasında paylaşımı yani context switch işinden sorumludur.

Yazımın devam eden kısmında Kotlin coroutine ile nasıl asenkron programlama yapabileceğimizden bahsedeceğim.

Coroutine nedir ?

Öncelikle belirtmem gereken konu, coroutine’lerin thread olmadığıdır. Kotlin ekibi coroutine’leri “ lightweight thread ” olarak tanımlasa da bu tanım communitynin algısına girebilmek için kullanılan bir benzetmedir. Coroutinei tanımlamak istersek thread tarafından yürütülen görev parçalarıdır diyebiliriz. Başlıktaki gif’den de anlaşılabileceği gibi herhangi bir thread x anında bir coroutine’i alıp işletebilir. Ancak bu threadin ilgili coroutine’i bitene kadar bekleyeceği anlamına gelmez. Bloklayıcı bir bölüme gelindiğinde (suspension point) thread coroutine’i bırakır ve başka bir coroutine’i işletmeye başlayabilir (context switch). Suspend olan görev, işini bitirdiğinde tekrar başka bir thread tarafından işletimine devam edilebilir. Böylelikle thread’ler bloklanmadan verimli bir şekilde kullanılmış olur. Bunu CoroutineDispatcher organize eder. CoroutineDispatcher context switching için suspend keyword’ü ile etiketlenmiş özel function’ ları kullanır. Coroutine ile kod örneklerine geçmeden önce suspend functionların iyi anlaşılması gerekmektedir.

Suspend function

Coroutine’lerin belli suspension pointlerde context switche girdiğinden bahsetmiştim. Bu noktaları derleyiciye geliştirici kendi deklare etmek durumundadır. Bu deklarasyonlar ilgili methodu suspend keywordü ile etiketleyerek sağlanır.

Özetle; suspend functionlar, mevcut threadi bloke etmeden coroutinin yürütülmesini askıya alır. Böylelikle thread başka bir corutinenin işletilmesine başlar ve cpu daha verimli bir şekilde kullanılmış olur.

Suspend functionlarla ilgili bir başka bilinmesi gereken konu da sıralı işletildikleridir. Yani dönüş değeri almadan ya da kodun geri kalanını çalıştırmadan önce, çağrılan suspend functionın yürütülmesini beklememiz gerekir. Bu durum asenkron operasyonların Kotlin ile ne denli kolay olduğuna bir kanıt niteliğindedir. Özetle, asenkron kod yazımının, normal yazımdan hiçbir farkı yoktur.


operation1 done after 1 second
operation2 done after 2 seconds and result: hello

Suspend functionlar diğer dillerin asenkron yazımdan farklı olarak, geriye özel veri tipleri dönmek zorunda değillerdir.

  • Java — Future, Flux, Mono, Single, Observable ve binlercesi :)
  • Javascrip t— Promise
  • C# — Task

Yeni bir coroutine başlatma

Kotlin standart kütüphanesi, coroutine başlatmak için coroutine builder denen yapıları barındırır. Coroutine builder, belirli bir suspend fonksiyonu sarmalayan ve işletilmesini sağlayan fonksiyonlardır. Yani, yeni bir coroutine başlatmak için kullanırız. Bunlardan başlıcalarına göz atacak olursak.

CoroutineScope.Async

Geriye değer dönen asenkron operasyonların işletilmesi için kullanılır. Beklenen sonucu almak için Deffered dönüş tipi kullanır. Deferred, Javadaki Future veya Javascript dilindeki Promise in karşılığıdır diyebiliriz_._ Sonucu beklemek için Deffered sonucun await methodunu çağırmak gerkir. Await bir suspend functiondır.

CoroutineScope.Launch

Geriye dönüş tipi gerektirmeyen, arka planda işletilecek operasyonlar için kullanılan builder tipidir. Fire and forget mantığı ile çalışabilecek işler için uygundur.


Hello World!

Runblocking

İlgili coroutine tamamlanana kadar, mevcut threadin bloke edilmesini sağlar. Coroutine içerisinde kullanılmamalıdır. Genelde blocking tarzda yazılmış koda coroutine scope bağlamak için ya da test amaçlı kullanılır. Yani normal dünya ile suspending dünya arasında köprü görevi görür. Bütün kod bloğu uçtan uca non-blocking tarzda yazılmışsa kullanımına gerek yoktur.

Kotlin ekibi coroutine scope içerisinde thread bloklayacak yaklaşımlardan kaçınılmasını önerir. Bu sebeple, runBlocking’in coroutine içerisinde kullanmaktan kaçınmalıyız. Eğer runBlocking kullanılacaksa, hiyerarşide daima en tepede yer almalıdır.

Eşzamanlı İşletim

Coroutineler eş zamanlı işletime de olanak sağlar. Aşağıdaki kod örneğinde eş zamanlı ve sıralı işletimin karşılaştırmasını görebilirsiniz.

operation1: 247 ms
operation2: 5112 ms

Dispatcher — I/O ve CPU bağımlı işler

Kotlin coroutine altyapısında dispatcher denen nesneler vasıtasıyla senkronizasyon yaptığından bahsetmiştik. Dispatcherların reactive frameworklerdeki (örn: rxJava) scheduler nesnesine karşılık geldiğini düşünebiliriz. Corotuineler CPU açısından hiçbir anlam ifade etmeyen basit görev abstractionlarıdır. Dispatcher, coroutin yürütmesinden sorumludur t anında bir coroutine’i alıp threadpoolda bir threade atayarak işletilmesini sağlar ve gerektiğinde coroutine’i suspend ederek threadin başka bir coroutine yürütmesini sağlayabilir. Bu bağlamda, dispatcherlar coroutine’leri CPU için anlamlı hale getirmiş olur diyebiliriz.

Ancak burada dikkat edilmesi gereken nokta ilgili işin networkden ya da filesystemden okuma yazma yapan I/O ağarlıklı bir iş mi yoksa yüksek aritmetik ve mantıksal operasyon barındıran CPU intensive bir iş mi olduğudur. Kotlin coroutine bu tip operasyonları I/O ya da CPU yoğun işlere özelleşmiş thread poollarda ele alır. Kotlin’e ilgili coroutine’in hangi kapsamda ele alıncağının bildirilmesi yazılım geliştiricinin sorumluluğundadır. Bunun için Kotlin, kotlinx.coroutines paketi içerisinde aşağıdaki öntanımlı dispatcherları barındırır.

  • Dispatchers.Default : Herhangi bir dispatcher belirtilmemişse, tüm coroutine üreticileri tarafından kullanılan varsayılan dispatcherdır. CPU yoğun görevlerin yürütülmesi için uygun seçimdir. (RxJava’daki Schedulers.Computation)
  • Dispatchers.IO : I/O yoğun işlemler için kullanılır. ( RxJava’daki Schedulers.IO)

I/O yoğun işe örnek olarak databaseden yaptığımız okuma yazmaları veya http çağrılarını, CPU yoğun işe ise hash operasyonu ya da görsel işlemeyi örnek gösterebiliriz. Eğer kurumsal bir uygulama geliştiriyorsak ağırlıklı olarak databaseden okuma yazma yaparız bu da I/O yoğun bir uygulamamız olduğu anlamına gelir. Yazımın önceki bölümlerinde bir I/O işlemi sürerken, threadin bloke edilmeksizin başka kaynaklara tahsis edilebileceğinden bahsetmiştim. Coroutine ile hedeflenen tam da budur ve bu durum coroutine kullanımının kurumsal uygulamalar için oldukça mantıklı olduğu anlamına gelir.

CPU bloklayan bir IO operasyonu

Non-Blocking IO operasyonu

Özet

Asenkron işlemler günümüzde çoğu yazılım geliştiricinin bir şekilde içerisinde yer aldığı ya da farkında olmadan yer almak zorunda olduğu bir durumdur. Örneğin, bir web uygulaması geliştiriyorsak concurrency bizim için kritiktir. İstemcilere sağlıklı bir hizmet verebilmek için, uygulamamızın birim zamanda karşılayabildiği istek sayısını ( throughput ) en yüksekte tutmamız gerekir. Veya bir client uygulaması geliştiriyorsak, kullanıcı deneyimi açısından, arkaplanda yaptığımız işlemlerin kullanıcının UI üzerinde yaptığı işlemleri bloke etmemesini sağlamamız gerekir. Önceleri bu problemlerin çözümü için multi-threading tekniklerini kullansak da senkronizasyon problemi ve kullanımdaki zorluklar, yazılım endüstrisini bu konuda yeni çözümlerin arayışına itmiştir. Bu bağlamda, java ekosisteminde rxJava , Spring’de Reactor , c#’da async/awai t, Golang’da goroutine gibi daha yüksek seviye çözümler yazılım geliştiricilerin hizmetine sunuldu. Bütün bu çözümlerin ortak noktası yazılım geliştiriciyi karmaşık thread operasyonlarından soyutlayarak, önceden optimize edilmiş bir ortam sunmaktır. Kotlin ekibinin sunduğu çözüm ise coroutine olmuştur. Yazımda kotlin coroutine kullanımına ve kavramlara dair genel bilgiler vermek istedim. Kotlin coroutine’leri kullanarak, I/O ağırlıklı bir uygulamanın, aynı kaynak kullanılarak daha fazla istek karşılayabilmesini sağlayabiliriz. Bunu uygulamasını ve karşılaştırmalı yük testlerini, önümüzdeki haftalarda yeni bir medium postu ile paylaşacağım. Yeni yazımda görüşene dek şimdilik hoşçakalın :)


Discussion

pic
Editor guide