Asenkron & Nonblocking Programlamada Coroutine’ler

Bugra AYDIN
7 min readOct 15, 2020

Gün geçtikçe bir şeylere daha hızlı bir şekilde ulaşmanın daha da fazla tutkunu oluyoruz. Teknoloji tarafında; haber sitelerinden, sosyal medya platformlarına, alışveriş sitelerinden, gündelik yaşamımızı planladığımız anlık senkronize olan mobil ve web uygulamalara kadar uzanıyor. İşimizi hızlıca halledip yolumuza bakmak istiyoruz.

Bunun için kullanıcıya sunulan uygulamalar kullanıcıyı bloklamadan işlemlerini hızlı bir şekilde yürütme amacını taşıyor. Bunun için birçok teknoloji ile farklı çözümler sunulmuştur. Başlıkta da belittiğim gibi kotlin dilinin bu tür durumlar için uygulanan asenkron & nonblocking’e farklı bir bakış açısı ile bize sunduğu çözüm ise; coroutine’dir.

Peki Coroutine nedir ?

Aslında iki kelimenin bileşmesi ile meydana gelmiştir: Coroutine(Cooperation + Routine). Coroutine’ler birbiri arasında koordine olabilen fonksiyonlardır. Kendi geliştiricileri tarafından da(Essentially, coroutines are light-weight threads.) light weight thread olarak isimlendirilse de aslında thread’in anlık yürüttüğü iş parçacıklarına verilen isimdir. Aslında bir concurrency çözümüdür. Suspendable olması ile thread eş zamanlı olarak coroutine’leri çalıştırabilir. Yani fonksiyonun çalışmasını durduruyoruz ve diğer işlerimize bakıyoruz oradan cevap geldiği zaman yolumuza devam ediyoruz. Gelin şimdi işin mutfağına girelim.

Neye ve nasıl çözüm sunar ?

  • Asenkron Operasyon

Java gibi dillerde asenkron bir işlem yapmak için en bildiğimiz yöntem ayrı bir thread oluşturup onun işlemini tamamlaması bizim de kendi yolumuza devam etmemiz oluyordu. Fakat thread oluşturmak bildiğimiz gibi maliyetli ve kısıtlı bir işlem. Ve thread’lerle uğraşmak, onları yönetmek apayrı bir sıkıntı. Coroutine’ler light weight’tir. Ve kaynak tüketimi açısından çok daha optimize bir yapıya sahiptir.

Coroutine Suspending Mechanism
  • Callback Hell

Asenkron bir network çağrısı yaptığımızda ondan gelen sonuca göre farklı bir network çağrısı yaptığımızı ondan gelenle de farklı bir network çağrısı diye giden bir flow’umuz olduğunu düşündüğümüzde; kod aşağıdaki gibi iç içe girmiş fonksiyonlara dönüşüyor ve bu bizim için hem okunaklılığı azaltıyor hem de geliştirmeyi zorlaştırıyor.

Traditional Nested Callbacks

Coroutine’leri yazılırken tıpkı senkron kod gibi yazıyorsunuz. Suspend olan fonksiyondan cevap gelmeden aşağıdaki fonksiyona geçmiyor. Bu sayede kodu daha kolay geliştirebiliyor ve okunaklılığı artırabiliyorsunuz.

Before (implemented by callback)

networkRequest { result ->
// Successful network request
databaseSave(result) { rows ->
// Result saved
}
}

After (implemented by coroutine)

val result = networkRequest()
databaseSave(result)
  • Memory Leak

Thread’ler maliyetli kaynaklardır ve thread oluşturduktan sonra bunun takibini doğru bir şekilde yapmak gerekir. Aksi halde kaynaklarınız yetersiz kalmaya ve sisteminiz istekleri karşılayamaz hale gelecektir . Coroutine’ler belli bir CoroutineScope içerisinde çalıştırılırlar. Eğerki scope cancel edilirse; o scope içerisindeki tüm coroutine’ler de cancel edilir ve gereksiz kaynak tüketiminden kurtulunmuş olunur.

Nested Coroutines/Jobs

Terimleri Açıklayalım

Coroutine’ler çözümü sunarken bazı kavramları da beraberinde getiriyor. İşin biraz sıkıcı ve daha soyut kısmı fakat örneklerle de desteklediğimizde hepsi yerine oturacaktır.

Suspend Function

Fonksiyonun suspendable olduğunu belirtmemizi sağlar. Coroutine dışarısında çağırılamaz. Thread’i bloklamadan coroutine’in askıya alınmasını sağlar. Bu sayede thread farklı bir coroutine’i execute edebilir. Yukarıda örnekte de gördüğümüz gibi suspend function’lar sıra ile execute edilir. Bu da asenkron kodu senkron gibi yazmamısı sağlar.

Job

launch, CoroutineBuilder’ı ile oluşturulur. İlgili kod bloğunun tamamlanmasından sorumludur. Coroutine’e bununla ulaşır ve cancel/join edebiliriz.

Coroutine Job States

CoroutineScope

Bütün coroutine’ler bir scope içerisinde oluşturulur. CoroutineScope aslında bir job oluşturur ve coroutine’in lifecycle’ını yönetmek için kullanılır. Bu job iptal edildiğinde bu scope içerisindeki tüm coroutine’ler iptal olur.

Aşağıdaki örnekteki gibi GlobalScope kullanmak uygulama boyunca kaynakları tüketmesi demektir. O yüzden olabildiğince GlobalScope kullanarak coroutine’leri execute etmekten kaçınmalıyız.

GlobalScope.launch{
doSomethingHeavy() // suspend function call
}

CoroutineBuilders

CoroutineBuilder’lar CoroutineScope’a bağlı ve CoroutineContext’i referans alacak şekilde coroutine oluşturmamızı sağlayan suspend olmayan normal fonksiyonlardır. 3 türü vardır:

  • runBlocking

Mevcut thread’i coroutine’in işi bitene kadar bloklar. Bir coroutine içerisinde kullanılmamalıdır. suspend function’ları blok’lu yazmak, main fonksiyonu tanımlamak ve test yazmak için kullanılır.

  • launch

CoroutineScope’un extension fonksiyonudur. Mevcut thread’i bloklamadan yeni bir coroutine oluşturur. async ile aynıdır fakat coroutine’in referansı olarak Job döndürür. Job cancel edildiğinde coroutine içerisindeki tüm job’lar cancel edilir.

  • async

CoroutineScope’un extension fonksiyonudur. launch gibi async de thread’i bloklamaz. Asenkron olarak operasyonu gerçekleştirir ve deferred object döner tıpkı javascript’teki promise gibi. Sonucu bekleyip deferred value’dan almak için await suspend fonksiyonunu çağırırız. Deferred objesi Job’tan türediği için cancel ettiğimizde coroutine de iptal edilir.

CoroutineContext

Coroutine’in mevcut bilgileri bu context üzerinde tutulur. Coroutine’ler her zaman CoroutineContext üzerinde execute edilir. CoroutineContext çeşitli elemanlardan oluşur:

  • Job
  • CoroutineDispatcher
  • CoroutineName
  • CoroutineExceptionHandler

CoroutineBuilder’lar CoroutineContext’i optional parametre olarak alırlar. CoroutineContext’i oluştururken aşağıda görüldüğü gibi optional olan bu parametreleri aralarına “+” koyarak geçebiliriz;

launch(Dispatchers.Default + CoroutineName("test") + Job()) {
println("I'm working in thread ${Thread.currentThread().name}")
}

CoroutineDispatchers

Coroutine’lerin thread’ler tarafından execute edildiğinden bahsetmiştik. CoroutineDispatchers hangi thread veya thread’lerin coroutine’i execute edeceğini belirler.

T anında bir coroutine’i alıp thread pool’dan bir thread’e atayarak işletilmesini sağlar ve gerektiğinde coroutine’i suspend ederek thread’in başka bir coroutine’i execute etmesini sağlar. Ardından suspend edilen işlem tamamlandığında aynı/farklı bir thread ile yoluna devam eder. Dispatcher türlerine bakacak olursak:

  • Unconfined: Coroutine’i bir thread ile sınırlandırmadığımız durumdur. Coroutine’i çağıran thread tarafından başlatılır, ta ki ilk suspension point(ilk suspend function) ile karşılaşanada kadar. suspend edilen fonksiyonun thread’i ile devam eder.
  • Confined: CoroutineDispatcher, coroutine’i specific bir thread veya bir thread pool ile sınırlandırmasıdır. Dispatcher default olarak CoroutineScope’tan miras alır. runBlocking içerisinde çağırdığımızda coroutine’i runBlocking’in thread’ini alır.
Unconfined: I'm working in thread main
main runBlocking: I'm working in thread main
Unconfined: After delay in thread kotlinx.coroutines.DefaultExecutor
main runBlocking: After delay in thread main
  • Default: Kompleks ve uzun süren hesaplamalar/işlemler için kullanılır.
  • IO: Veri alış verişi için kullanılır. Network operasyonları, veritabanından veri okuma, dosyadan okuyup yazma gibi.
  • Main: Coroutine’i main thread ile execute edilir. Daha çok coroutine içerisinde UI ile alakalı işler yapılacağında kullanılır.

withContext

Aynı coroutine içerisinde, coroutine’in context’ini değiştirmek için kullanılır. Yani thread’ler arasında switch yapmamızı sağlar diyebiliriz. Bir suspend function’dır. Bu yüzden mevcut thread’i bloklamaz. Pure value döner. Bizim değer dönmesi için kullandığımız async/await yerine withContext kullanılması önerilir.

async/await

val resultOne= async { suspendFunction1() }
val resultTwo= async { suspendFunction2() }
val combinedResult = resultOne.await() + resultTwo.await()

withContext

val resultOne = withContext(Dispatchers.IO){ suspendFunction1() }
val resultTwo = withContext(Dispatchers.IO){ suspendFunction2() }
val combinedResult = resultOne + resultTwo

Elleri kirletmeden öğrenmek olmaz :)

Coroutine’in nedir ne değildirini, neyi çözdüğünü ve nasıl çözüm sunduğunu ve bu çözümü sunarken ne gibi kavramlar ile birlikte geldiğinden bahsettik. Şimdi örnekler ile pekiştirelim.

Coroutine Builders & Suspend Functions

CoroutineBuilder’lar ve bunların ana thread’i bloklamamasından, suspend function’ların sıralı olarak çağırıldığından ve farklı thread pool’a geçiş yapmaktan bahsetmiştik. Aşağıdaki örnek ile adım adım inceleyelim:

Basic Coroutine Example

# 1: main() fonksiyonunu runBlocking coroutine’i ile başlatıyoruz.
# 2: launch ile thread’i bloklamayan ilk coroutine’i oluşturuyoruz.
# 3: launch ile simule ettiğimiz network çağrısı için farklı bir thread pool(Dispatchers.IO)’da çalışması gerektiğini belirterek coroutine oluşturuyoruz.
# 3.1: IO thread’i ile ilk suspend function call(delay bir suspend function’dır) yapılır. Suspend’i gördüğü anda coroutine askıya alınır. İşlem tamamlanana kadar bir alt satıra geçilmez. Thread farklı işler için yoluna devam eder. 2 saniyelik delay bittikten sonra bir alt satıra geçer
# 3.2: delay işlemi tamamlandıktan sonra simule ettiğimiz 3 saniyelik network çağırı yapılır.
# 3.3: İkinci coroutine; 2 suspend function’ın toplam zamanı kadar bir zaman harcamaktadır. Bu da bize suspend function’ların sıralı çalıştığını göster.
#3.4: job.join() ile o coroutine’in beklenmesini sağlarız. Bloklarız.
# 4: Toplam 2 coroutine’in execution’ı en fazla hangisi sürüyorsa onun kadardır. Çünkü asenkron olarak birbirlerini beklemeden çağırılırlar.

Yazdığımız kodun adım adım çıktısı:

Function thread name main @coroutine#1 
Coroutine thread name 2 DefaultDispatcher-worker-1 @coroutine#3 Making long-running network call
Coroutine thread name 1 main @coroutine#2
Coroutine two completion time : 5006
Total time 5027

Coroutine Cancellation

Coroutine’lerin cancel edilebilir olduğunu söylemiştik. Coroutine içerisindeki suspend function’lar cancel edilebilir. Bu şu şekilde olur; coroutine’in cancel edilip edilmediğini kontrol ederler ve cancel edildi ise CancellationException fırlatırlar. Böylece job iptal edilir. Eğerki coroutine bir hesaplama/dosyadan okuma vs(suspendable olmayan bir işlem) ile meşgul ise o zaman cancellation durumunu kontrol edemez. Aşağıdaki örnekte job cancel edildiği halde işlemin sonsuza kadar devam ettiğini görebiliriz.

job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm sleeping 3 ...
job: I'm sleeping 4 ...
...
..

Böyle devam eder ve kaynaklarımızı tüketmesini istemeyiz. O yüzden bunu manuel olarak içeride kontrol etmemiz gerekiyor. Coroutine’in cancel edilip edilmediğini kontrol etmek için scope’un extension’ı olan isActive flag’ini kullandığımızda artık coroutine’in devam etmediğini görebiliriz.

job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.

Coroutine withTimeout

Coroutine’ler ile işlem için maksimum bir zaman ataması yapabilir ve o zaman aşımına ulaşılmış ise, iş’ten çıkılır. Bunu yapan withTimout suspend fonksiyonudur. Eğer ki timeout oldu ise; CancellationException’ın sub class’ı olan TimeoutCancellationException fırlatılır. Böylece işlem iptal olur. Main thread ile üzerinde çalıştırılırsa stack trace’e exception’ı yazar.

Burada exception atılıyor fakat farklı bir thread pool (Dispatchers.Default)’da çalıştırıldığı için stack trace’e yazılmıyor. 2 saniyelik bir timeout süresi var fakat 3 saniyelik bir network çağırısı simule ediliyor ve network çağrısının aşağısındaki println fonksiyonu çağırılmadan coroutine’den çıkılıyor.

Network call starting. 
Making long-running network call.
Ending long running network call.

Özetle

Uygulamalardaki verimlilik her geçen gün daha da kıymetli hale geliyor. Daha fazla isteği karşılamak, daha hızlı cevap vermek, kullanıcıyı bloklamamak ve az kaynak tüketimi. Bu yazıda Kotlin’in concurrency verimliliği için çıkarttığı coroutine’lerin neye nasıl çözüm sunduğunu, yapısını, bununla birlikte gelen kavramları örneklerle inceledik. Umarım faydalı olmuştur.

Bir sonraki yazıda görüşmek üzere

Bir hatamız oldu ise affola :)

Referanslar

Coroutine ve kavramlarından kısa ve öz örneklerin bulunduğu güzel dökümante edildiği bir blog yazısı:

Kotlin’in coroutine üzerine github dökümantasyonu:

--

--