Introduction to Kotlin Coroutines

Geliştirme yaptığımız platform hangisi olursa olsun ister backend, desktop, mobile applications etc her zaman istediğimiz aslında istediğimiz şey hep kullanıcımıza kesintisiz bir akış sunmak, uygulamamızı düzgün scale etmek. Bunu birçok farklı dilde farklı şekillerde yapabiliriz. Kotlin’in bize sunduğu çözüm ise Coroutine’ler.

Tabi yıllardır kullandığımız farklı çözümler mevcut. Hepsinin kendi avantaj ve dezavantajları var.En klasik çözümlerden biri Threading. Maliyetli bir yöntem olması dışında da tüm dillerde bulunmuyor, mesela js. Debug etmesinin kolay olmaması gibi problem de taşıyor. Callbackler bi diğer yaygın çözüm. Callback’deyse response geldiğinde çağıran yeri trigger ediyoruz ancak çok fazla nested callback yaratmamız ve kodu okunmaz hale getirmemiz söz konusu. Futures and promises, Ben açıkçası bunu kullanmadım burada biraz literatürün yalancısyım ancak yine kodlama şeklimizin farklılaşması söz konusu. Reactive Extentions, bu en çok alışık olunan yöntem olabilir Android geliştiriciler tarafından. Yine kodlamamızı biraz değiştirmemiz gerekiyor. Temel olarak observable pattern üzerine kurulu. Netflix’in .NET’deki RX’i Java’ya uyarlaması sonucu popülerlik kazanmıştır. Avantajı çok fazla platformla uyumlu olması. Coroutines ise Kotlin dışındaki dillerde de var. Temel olarak bir fonksiyonun herhangi bi anda çalışmasını durdurup, sonrasında devam etmesi özelliğini taşıyor. Okunurluk olarak ise snychronized olarak yazdığımız programdan bir farkı bulunmuyor. Biraz daha toparlarsak, Coroutine lightweight bi thread aslında. Paralel çalışır. Thread’den en büyük farkı almost free olarak tanımlanması. Thread’deki maliyetten kurtulmuş oluyoruz.

println("Start")

// Start a coroutine
GlobalScope.launch {
    delay(1000)
    println("Hello")
}

Thread.sleep(2000) // wait for 2 seconds
println("Stop")

Peki nasıl görünüyor. Coroutine launch edildikten sonra delay’in çağrıldığını ve sonrasında ise print’in olduğunu görüyoruz yukarıdaki örnekte. delay 1 snliğine coroutine’in resume olmasını ve kodun coroutine bloğu dışında akıp 1 sn sonra hello’yu print etmesini sağlıyor. Delay’in sleep’den farklı thread’i bloklamıyor olması. Buradaki fark delay i sleep’i kullandığımız gibi kullanamayız. Coroutine’in dışında kullansaydık ise suspend fonksiyonları coroutine dışında kullanamayız hatası alacaktık.

Aşağıda gördüğümüz kod thread kullanıyor, 1 milyon a kadar sayıları topluyor. Sabrımızın beklemekten tükeneceği kadar uzun süren bi noktaya gidebiliriz.

val c = AtomicLong()

for (i in 1..1_000_000L)
    thread(start = true) {
        c.addAndGet(i)
    }

println(c.get())

Bunu coroutine’le yapsaydık ise aşağıdaki gibi görünekti. Yalnız bunda da bir problem var. Println çağrıldığı noktada bazı coroutine’ler tamamlanmamış olabilir.

val c = AtomicLong()

for (i in 1..1_000_000L)
    GlobalScope.launch {
        c.addAndGet(i)
    }

println(c.get())

Bunu nasıl fixleriz dersek de aslında sonucunu bekleyerek. Coroutine’i launch değil de async olarak start ettiğimizde geriye defererred tipinde bir dönüşü olacak ki bunun da await yani dönen sonucu almamızı sağlayan bir fonksiyonu bulunmakta. Burada await tüm hesaplamanın sonuna kadar suspend etmesi gerektiği için coroutine dışından çağıramıyoruz. O yüzden await ve computationı run blocking içerisine aldık. (örnek aşağıda)

val deferred = (1..1_000_000).map { n ->
    GlobalScope.async {
        n
    }
}
val sum = deferred.sumBy { it.await() }

Suspending function suspending function diyoruz. Eğer suspending function kullanmasaydık yani biri bitmeden diğerini başlamasaydı A B’yi bloklayacaktı. A çalışmaya başladığından durdurulup B tamamlanıp A çalışmaya devam edebilir.


https://medium.com/@elye.project/understanding-suspend-function-of-coroutines-de26b070c5eda

Şimdiye kadar hep delay örneğinden gittik ama biz kendimiz de suspend keywordünü başına eklediğimizde suspend function yazabiliriz. Suspend functionlar runblocking, launch ya da async içerisinde olmalıdır. 

suspend fun workload(n: Int): Int {
    delay(1000)
    return n
}

GlobalScope.async {
    workload(n)
}

Mesela bir sayfada başlattınız sayfadan çıkarken iptal etmek isteyebilirsiniz ya da gerçekten belli bir süre sonra iptal etme ihtiyacınız olabilir. Launch geriye job döndürüyor. Bunun üzerinden de cancel çağırılarak iptal edilebiliyor.

val job = launch {
    repeat(1000) { i ->
       println("job: I'm sleeping $i ...")
       delay(500L)
    }
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")

https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html
job.cancel() // cancels the job
job.join() // waits for job's completion
println("main: Now I can quit.")

https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html

 Bazı durumlarda da timeoutla işi sonlandırmak gerekebilir. Bu durumda withtimeout’u kullanabiliriz.

withTimeout(1300L) {
    repeat(1000) { i ->
            println("I'm sleeping $i ...")
        delay(500L)
    }
}

https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html

Yukarıdaki bazı örneklerde async, bazılarında ise launch kullandık. Async’in launch’dan farklı olarak geriye bir result döndüğünden bahsetmiştik. İkisinin arasındaki fark async tüm diğer threadlerle beraber kullanılabilir. Aynı anda başlattığımızda tabi await toplam aldığı için her ikisinin de bitmesini bekleyecek ama en uzun süren kadar sürmüş olacak.

Biraz da suspend fonksiyonları hangi senaryolarda kullanabileceğimizden bahsedelim.

Sequential by default: Diyelim ki anlamlı işler yapan 2 suspend fonksiyonumuz var.

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here
    return 13
}
suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // pretend we are doing something useful here, too
    return 29
}

https://kotlinlang.org/docs/reference/coroutines/composing-suspending-functions.html

By default coroutine’ler sequantial’dır. Ve ikisinin toplamı kadar sürede tamamlanır. Genelde birbirine bağlımlılığı olan işlerde bu yapı kullanılır. (The answer is 42 – Completed in 2011 ms)

val time = measureTimeMillis {
    val one = doSomethingUsefulOne()
    val two = doSomethingUsefulTwo()
    println("The answer is ${one + two}")
}
println("Completed in $time ms")

https://kotlinlang.org/docs/reference/coroutines/composing-suspending-functions.html

Concurrent using async: Concurency bilgisayar biliminde farklı program parçacıklarının, algoritmaların ya da bir problemin kısmi sıralı ya da sırasız bir şekilde çalıştırıldığında sonucu etkilememesidir. Diyelim ki iki methodu call etmek için birbirinin result’ına ihtiyacımız yok. Mümkün olan en kısa sürede işlemi tamamlamak istiyoruz. (The answer is 42 – Completed in 1017 ms)

val time = measureTimeMillis {
    val one = async { doSomethingUsefulOne() }
    val two = async { doSomethingUsefulTwo() }
    println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")

https://kotlinlang.org/docs/reference/coroutines/composing-suspending-functions.html

Structured concurrency with async: Aşağıdaki örnekteki gibi corrurrent bir şekilde alacak bir method extract edelim. Async coroutine scope’un bir extentionı olduğu için bizim ona coroutine scope içerisinde ihtiyacımız var. Bu şekilde iki methoddan biri hata alırsa exception fırlatır ve içerde launch edilen tüm coroutine’ler iptal edilmiş olur. (The answer is 42 – Completed in 1017 ms)

suspend fun concurrentSum(): Int = coroutineScope {
    val one = async { doSomethingUsefulOne() }
    val two = async { doSomethingUsefulTwo() }
    one.await() + two.await()
}

https://kotlinlang.org/docs/reference/coroutines/composing-suspending-functions.html
val time = measureTimeMillis {
    println("The answer is ${concurrentSum()}")
}
println("Completed in $time ms")

https://kotlinlang.org/docs/reference/coroutines/composing-suspending-functions.html

Çok kısaca channel’lara da yer verirsek, channel’lar coroutine’ler arasında data geçirilmesini sağlayan communication primitiveleri’dir. Chanellara bir ya da birden fazla coroutine data gönderebilir yine bir ya da birden çok da alabilir. Gönderenlere producer alanlara ise consumer denir.


https://play.kotlinlang.org/hands-on/Introduction%20to%20Coroutines%20and%20Channels/08_Channels

https://play.kotlinlang.org/hands-on/Introduction%20to%20Coroutines%20and%20Channels/08_Channels

Coroutine’ler CoroutineContext adı verilen bir context’de execute edilir. Coroutine context de çeşitli elementlerin bir setidir aslında. Bunlardan başlıcaları da job ve dispatcherlar. Dispatcher’ler coroutine çalışırken hangi thread’i kullanacağını belirtirler. Tüm coroutine builder’lar Dispacher tanımı optional olarak  alırlar. Tipleri ise aşağıdaki gibi.

Dispatchers.Main

  • Main thread
  • Use it for: UI operations

Dispatchers.Default

  • Common pool of shared background threads
  • Use it for: computing-intensive coroutines

Dispatchers.IO

  • Shared pool of on-demand created threads
  • Use it for: IO-intensive blocking operations

Dispatchers.Unconfined

  • Doesn’t confine the coroutine to any specific thread
  • Don’t use it in code

Exception handling:

fun main() = runBlocking {
    val job = GlobalScope.launch {
        println("Throwing exception from launch")
        throw IndexOutOfBoundsException() // Will be printed to the console by Thread.defaultUncaughtExceptionHandler
    }
    job.join()
    println("Joined failed job")
 }

https://kotlinlang.org/docs/reference/coroutines/exception-handling.html
fun main() = runBlocking {
    val deferred = GlobalScope.async {
        println("Throwing exception from async")
        throw ArithmeticException() // Nothing is printed, relying on user to call await
    }
    try {
        deferred.await()
        println("Unreached")
    } catch (e: ArithmeticException) {
        println("Caught ArithmeticException")
    }
}

https://kotlinlang.org/docs/reference/coroutines/exception-handling.html

Çok temel bir giriş olarak suspend fonksiyonlar bu şekilde, daha detaylı bilgi linkteki sunumu v bu yazıyı yazmamı sağlayan faydalı bulduğum birkaç linki de aşağıda paylaşıyorum.

PS: Eğer buraya kadar okuduysanız ve yazıyı beğendiyseniz, yıldızlayabilir ve de paylaşabilirsiniz. 😀

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Blog at WordPress.com.

Up ↑