Coroutine’ler, Kotlin’in asenkron kod yazma yöntemi. Senkron görünen ama thread’leri bloke etmeden çalışan kod yazmanızı sağlıyorlar. Bu rehberde suspend fonksiyonlardan structured concurrency’e kadar coroutine’lerin temellerini ele alıyoruz.

Bu rehberde şunları öğreneceksiniz:

  • Suspend fonksiyonlar
  • launch: sonucu beklemeden başlat
  • async/await: sonuç al
  • Dispatcher’lar
  • coroutineScope
  • Structured concurrency
  • Job ve iptal etme
  • Zaman aşımı (timeout)
  • Pratik örnekler

Coroutine Nedir?

Bir coroutine, hafif bir thread’dir. Tek bir thread üzerinde binlerce coroutine çalıştırabilirsiniz. Thread’lerin aksine, coroutine’ler ucuza oluşturulur ve üzerinde çalıştıkları işletim sistemi thread’ini bloke etmez.

Normal fonksiyonlar baştan sona duraksamadan çalışır. Coroutine’ler ise duraklayabilir (suspend) ve daha sonra devam edebilir (resume). Bir coroutine askıya alındığında, thread başka işler yapmakta serbesttir.

// Normal fonksiyon: thread'i bloke eder
fun fetchData(): String {
    Thread.sleep(1000) // Thread'i 1 saniye bloke eder
    return "data"
}

// Suspend fonksiyon: bloke etmeden duraklar
suspend fun fetchData(): String {
    delay(1000) // 1 saniye duraklar, thread serbest kalır
    return "data"
}

Coroutine Kurulumu

Coroutine bağımlılığını build.gradle.kts dosyanıza ekleyin:

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")
}

Suspend Fonksiyonlar

Bir suspend fonksiyon, duraklayıp devam edebilen bir fonksiyondur. suspend anahtar kelimesiyle işaretlenir. Bir suspend fonksiyonu yalnızca başka bir suspend fonksiyondan ya da bir coroutine içinden çağırabilirsiniz.

suspend fun fetchUserName(): String {
    delay(100) // Bir ağ çağrısını simüle eder
    return "Alex"
}

suspend fun fetchUserAge(): Int {
    delay(100) // Başka bir ağ çağrısını simüle eder
    return 28
}

Suspend fonksiyonlar başka suspend fonksiyonları çağırabilir:

suspend fun fetchUserProfile(): String {
    val name = fetchUserName()
    val age = fetchUserAge()
    return "$name, age $age"
}

Bu örnekte önce fetchUserName(), ardından fetchUserAge() çalışır. Sırayla çalışırlar. Toplam süre yaklaşık 200 ms’dir (100 ms + 100 ms). İlerleyen bölümlerde bunları async ile paralel çalıştırmayı öğreneceksiniz.

runBlocking: Köprü

runBlocking, bir coroutine oluşturur ve o coroutine bitene kadar mevcut thread’i bloke eder. Suspend fonksiyonları çağırmak için main() fonksiyonlarında ya da testlerde kullanın.

fun main() = runBlocking {
    val profile = fetchUserProfile()
    println(profile) // "Alex, age 28"
}

runBlocking‘i üretim kodunda kullanmayın. Thread’i bloke eder, bu da coroutine’lerin amacını boşa çıkarır. Sadece main() fonksiyonunda veya testlerde kullanın.

launch: Sonucu Beklemeden Başlat

launch, yeni bir coroutine başlatır ve bir sonuç döndürmez. İptal etmek veya beklemek için kullanabileceğiniz bir Job döndürür.

Geriye bir sonuç almanıza gerek olmadığında launch kullanın.

coroutineScope {
    launch {
        delay(100)
        println("Task 1 done")
    }

    launch {
        delay(200)
        println("Task 2 done")
    }

    println("Both tasks started")
}
println("Both tasks finished")

Çıktı:

Both tasks started
Task 1 done
Task 2 done
Both tasks finished

İki coroutine de hemen başlar. coroutineScope, devam etmeden önce ikisinin de bitmesini bekler.

async/await: Sonuç Al

async, yeni bir coroutine başlatır ve bir Deferred<T> döndürür. Sonucu almak için .await() çağırın.

Geriye bir sonuca ihtiyacınız olduğunda async kullanın.

coroutineScope {
    val nameDeferred: Deferred<String> = async {
        delay(100)
        "Alex"
    }

    val ageDeferred: Deferred<Int> = async {
        delay(100)
        28
    }

    // İkisi de paralel çalışır: toplam süre ~100 ms, 200 ms değil
    val name = nameDeferred.await()
    val age = ageDeferred.await()
    println("$name is $age years old")
}

Temel fark şu: launch, yan etkiler (loglama, veritabanına kaydetme) içindir. async, bir değer döndüren hesaplamalar içindir.

launch ve async Karşılaştırması

Özelliklaunchasync
DöndürdüğüJobDeferred<T>
Sonuç alır mıHayırEvet, .await() ile
Kullanım alanıYan etkilerHesaplamalar
İstisna (exception)Hemen yayılır.await() çağrısında yayılır

Paralel Ayrıştırma

async‘in en yaygın kullanım alanı, birden fazla görevi paralel çalıştırıp sonuçlarını birleştirmektir:

suspend fun fetchFromMultipleSources(): List<String> {
    return coroutineScope {
        val source1 = async { delay(100); listOf("Item A", "Item B") }
        val source2 = async { delay(150); listOf("Item C", "Item D") }
        val source3 = async { delay(80); listOf("Item E") }

        source1.await() + source2.await() + source3.await()
    }
}

Üç kaynak da paralel veri çeker. Toplam süre yaklaşık 150 ms’dir (en yavaş olan), 330 ms değil (100 + 150 + 80).

Dispatcher’lar

Dispatcher’lar, bir coroutine’in hangi thread’de çalışacağını kontrol eder. Kotlin, birkaç yerleşik dispatcher sunar:

DispatcherThreadKullanım Alanı
Dispatchers.DefaultPaylaşılan thread havuzuCPU-yoğun işler (sıralama, ayrıştırma)
Dispatchers.IOPaylaşılan thread havuzu (daha büyük)I/O işlemleri (ağ, dosyalar)
Dispatchers.MainAna/UI thread’iUI güncellemeleri (sadece Android)
Dispatchers.UnconfinedÇağıran threadTest, özel durumlar
coroutineScope {
    launch(Dispatchers.Default) {
        println("Default: ${Thread.currentThread().name}")
    }

    launch(Dispatchers.IO) {
        println("IO: ${Thread.currentThread().name}")
    }
}

withContext

withContext, bir kod bloğu için dispatcher’ı değiştirir. Mevcut coroutine’i duraklatır ve bloğu verilen dispatcher üzerinde çalıştırır.

suspend fun computeResult(): String {
    return withContext(Dispatchers.Default) {
        // Default dispatcher üzerinde ağır hesaplama
        (1..1_000_000).filter { it % 2 == 0 }.sum().toString()
    }
}

Bir fonksiyon içinde dispatcher değiştirmeniz gerektiğinde withContext kullanın. Örneğin, veriyi Dispatchers.IO üzerinde çekip ardından Dispatchers.Default üzerinde işleyin.

coroutineScope

coroutineScope, yeni bir scope oluşturur ve tüm alt coroutine’lerin bitmesini bekler. Herhangi bir alt coroutine başarısız olursa, diğer tüm alt coroutine’ler iptal edilir.

coroutineScope {
    launch {
        delay(100)
        println("Child 1 done")
    }
    launch {
        delay(200)
        println("Child 2 done")
    }
    println("Parent continues while children run")
}
println("All children finished")

coroutineScope bir suspend fonksiyondur, dolayısıyla thread’i bloke etmez. Sadece tüm alt coroutine’ler bitene kadar duraklar.

Structured Concurrency

Structured concurrency, coroutine yönetimini güvenli ve öngörülebilir kılan bir tasarım ilkesidir. Dört kuralı var:

  1. Her coroutine’in bir parent scope’u vardır. Bir scope olmadan coroutine oluşturamazsınız.
  2. Parent, tüm çocuklarını bekler. Parent coroutine, tüm alt coroutine’ler tamamlanana kadar tamamlanmaz.
  3. Parent’ın iptali çocukları da iptal eder. Parent iptal edilirse, tüm alt coroutine’ler otomatik olarak iptal edilir.
  4. Bir çocuğun başarısızlığı parent’ı ve kardeşlerini iptal eder. Bir çocuk bir istisna fırlatırsa, parent ve diğer tüm çocuklar iptal edilir.
coroutineScope {
    val job1 = launch {
        delay(100)
        println("Job 1 done")
    }
    val job2 = launch {
        delay(200)
        println("Job 2 done")
    }
    val result = async {
        delay(150)
        42
    }

    println("Result: ${result.await()}")
    // job1 ve job2 otomatik olarak beklenir
}
println("Everything is done")

Structured concurrency olmadan, her coroutine’i elle takip etmeniz ve hepsinin bittiğinden ya da iptal edildiğinden emin olmanız gerekirdi. Structured concurrency bunu sizin için hallediyor.

Job

launch, bir Job döndürür. Coroutine’i kontrol etmek için bunu kullanabilirsiniz:

coroutineScope {
    val job = launch {
        repeat(5) { i ->
            println("Working $i...")
            delay(100)
        }
    }

    delay(250) // Bir süre çalışmasına izin ver
    println("Cancelling job...")
    job.cancel()
    job.join() // İptalin tamamlanmasını bekle
    println("Job cancelled: ${job.isCancelled}") // true
}

Job özellikleri:

ÖzellikAçıklama
isActiveCoroutine hâlâ çalışıyor mu?
isCompletedCoroutine bitti mi?
isCancelledCoroutine iptal edildi mi?

Job fonksiyonları:

FonksiyonAçıklama
cancel()Coroutine’i iptal et
join()Coroutine’in bitmesini bekle
cancelAndJoin()İptal et ve bekle

İptal Etme

Coroutine’lerde iptal etme kooperatiftir (cooperative). Coroutine, iptal edilip edilmediğini kendisi kontrol etmelidir. kotlinx.coroutines içindeki tüm suspend fonksiyonlar (delay, yield gibi) iptali otomatik olarak kontrol eder.

Suspend noktası olmayan CPU-yoğun işler için isActive veya ensureActive() kullanın:

val job = launch {
    var i = 0
    while (isActive) { // Coroutine'in hâlâ aktif olup olmadığını kontrol et
        i++
        // CPU-yoğun iş...
    }
    println("Loop ended at $i")
}

delay(10)
job.cancel()
job.join()

try-finally ile Temizlik

Bir coroutine iptal edildiğinde kaynakları temizlemek için try-finally kullanın:

val job = launch {
    try {
        repeat(100) { i ->
            println("Processing $i...")
            delay(50)
        }
    } finally {
        // Bu her zaman çalışır, iptal durumunda bile
        println("Cleaning up resources...")
    }
}

delay(130)
job.cancel()
job.join()

finally bloğu, coroutine iptal edilse bile çalışır. Dosyaları kapatmak, bağlantıları serbest bırakmak ya da başka temizlik işleri yapmak için burayı kullanırsınız.

İptal Edilemeyen Blok

finally bloğunda bir suspend fonksiyon çağırmanız gerekiyorsa, withContext(NonCancellable) kullanın:

finally {
    withContext(NonCancellable) {
        delay(100) // Bu, NonCancellable olmadan başarısız olur
        println("Saved progress to database")
    }
}

Zaman Aşımı

withTimeout, çok uzun sürerse coroutine’i iptal eder. TimeoutCancellationException fırlatır:

try {
    withTimeout(200) {
        delay(500) // Çok uzun sürüyor
        println("Done")
    }
} catch (e: TimeoutCancellationException) {
    println("Timed out!")
}

withTimeoutOrNull, istisna fırlatmak yerine null döndürür:

val result = withTimeoutOrNull(200) {
    delay(500)
    "Done"
}
println(result) // null

val result2 = withTimeoutOrNull(200) {
    delay(100)
    "Done"
}
println(result2) // "Done"

Zaman aşımı bir hata değil de normal bir durum olduğunda withTimeoutOrNull kullanın.

Pratik Örnek: Kullanıcı Verisi Çekme

Gerçek dünyadan bir desen: birden fazla kaynaktan veriyi paralel çekmek:

data class UserData(
    val name: String,
    val posts: List<String>,
    val followers: Int
)

suspend fun fetchUserData(userId: String): UserData {
    return coroutineScope {
        val name = async { fetchUserName(userId) }
        val posts = async { fetchUserPosts(userId) }
        val followers = async { fetchFollowerCount(userId) }

        UserData(
            name = name.await(),
            posts = posts.await(),
            followers = followers.await()
        )
    }
}

Üç API çağrısı da paralel çalışır. Biri başarısız olursa, diğerleri otomatik olarak iptal edilir (structured concurrency sayesinde).

Pratik Örnek: Öğeleri Eş Zamanlı İşleme

Bir öğe listesini paralel işleme:

suspend fun processItemsConcurrently(items: List<String>): List<String> {
    return coroutineScope {
        items.map { item ->
            async {
                delay(50) // İşlemeyi simüle et
                item.uppercase()
            }
        }.map { it.await() }
    }
}

// Kullanım
val result = processItemsConcurrently(listOf("hello", "world", "kotlin"))
println(result) // [HELLO, WORLD, KOTLIN]

Tüm öğeler aynı anda işlenir. Toplam süre yaklaşık 50 ms’dir (tek bir delay), 150 ms değil (3 x 50 ms).

Pratik Örnek: Gecikmeli Yeniden Deneme

Üstel geri çekilme (exponential backoff) ile başarısız bir işlemi yeniden deneme:

suspend fun <T> retryWithDelay(
    times: Int = 3,
    initialDelay: Long = 100,
    factor: Double = 2.0,
    block: suspend () -> T
): T {
    var currentDelay = initialDelay
    repeat(times - 1) { attempt ->
        try {
            return block()
        } catch (e: Exception) {
            println("Attempt ${attempt + 1} failed: ${e.message}")
        }
        delay(currentDelay)
        currentDelay = (currentDelay * factor).toLong()
    }
    return block() // Son deneme
}

Gecikme her başarısızlıktan sonra iki katına çıkar: 100 ms, 200 ms, 400 ms. Ağ yeniden denemeleri için yaygın bir desen.

Türkiye’nin Orta Segment Android Telefonlarında Coroutine’lerin Pratik Etkisi

Türkiye’de satılan Android telefonların büyük çoğunluğu, üst segment değil, orta segment (mid-range) cihazlar: sınırlı RAM, daha yavaş CPU çekirdekleri ve genellikle daha küçük bir batarya. Bu telefonlarda coroutine’leri doğru kullanmak, kağıt üzerinde bir “en iyi pratik” değil, doğrudan kullanıcı deneyimi meselesi. GlobalScope.launch ile başlatılıp asla iptal edilmeyen bir coroutine, kullanıcı ekrandan ayrıldıktan sonra bile arka planda çalışmaya devam edebilir; bu da orta segment bir telefonda hem bataryayı tüketir hem de sınırlı RAM’i gereksiz yere işgal eder. viewModelScope ya da lifecycleScope gibi yaşam döngüsüne bağlı scope’ları kullanmak (bir sonraki yazıda ele alacağımız ViewModel rehberinde bunu detaylandırıyoruz), ekrandan çıkıldığında ilgili işin otomatik iptal edilmesini sağlıyor. Aynı şekilde, Dispatchers.IO üzerinde çalışması gereken bir ağ çağrısını yanlışlıkla ana thread’de (Dispatchers.Main) çalıştırmak, üst segment bir telefonda fark edilmeyecek küçük bir donmaya yol açarken, orta segment bir cihazda uygulamanın “ANR” (Application Not Responding) uyarısı vermesine kadar gidebilir. Kısacası: coroutine’leri doğru scope’larda başlatmak ve gereksiz paralel işi engellemek, Play Store’daki kullanıcı yorumlarında “telefonumu ısıtıyor” veya “bataryayı hızlı bitiriyor” şikayetlerinin önüne geçmenin en ucuz yolu.

Sık Yapılan Hatalar

Hata 1: Üretimde runBlocking Kullanmak

// KÖTÜ: thread'i bloke eder
fun getData(): String = runBlocking {
    fetchData()
}

// İYİ: suspend olarak bırak
suspend fun getData(): String {
    return fetchData()
}

Hata 2: Structured Concurrency’i Unutmak

// KÖTÜ: coroutine sızıntısı, parent scope yok
GlobalScope.launch {
    doSomething()
}

// İYİ: coroutineScope veya yaşam döngüsüne bağlı bir scope kullan
coroutineScope {
    launch {
        doSomething()
    }
}

Hata 3: Paralel Olması Gerekirken Sıralı Çalıştırmak

// YAVAŞ: sırayla çalışır (200 ms)
suspend fun fetchBoth(): Pair<String, Int> {
    val name = fetchUserName() // 100 ms
    val age = fetchUserAge()   // 100 ms
    return Pair(name, age)
}

// HIZLI: paralel çalışır (100 ms)
suspend fun fetchBoth(): Pair<String, Int> = coroutineScope {
    val name = async { fetchUserName() }
    val age = async { fetchUserAge() }
    Pair(name.await(), age.await())
}

Özet

KavramAçıklama
suspendDuraklayıp devam edebilen bir fonksiyonu işaretler
launchBir coroutine başlatır, Job döndürür (sonuç yok)
asyncBir coroutine başlatır, Deferred<T> döndürür (sonuçlu)
runBlockingCoroutine bitene kadar thread’i bloke eder
coroutineScopeScope oluşturur, tüm çocukları bekler
DispatchersCoroutine’in hangi thread’de çalışacağını kontrol eder
withContextBir blok için dispatcher’ı değiştirir
JobBir coroutine’i kontrol eder ve izler
cancel()Bir coroutine’i iptal eder (kooperatif)
withTimeoutÇok yavaşsa iptal eder (fırlatır)
withTimeoutOrNullÇok yavaşsa iptal eder (null döndürür)

İlgili Yazılar