Kotlin Coroutines Introduction - mariamaged/Java-Android-Kotlin GitHub Wiki

Kotlin Coroutines - Introduction

Kotlin is an ever-evolving language, with a steady stream of new releases.

  • 2018 saw the release of Kotlin 1.3, and perhaps the pre-eminent feature in that release was the coroutine system.
  • Google already is supporting coroutines in some of the Android Jetpack Kotlin extension libraries and will expand upon those in the upcoming years.

The Problems

Doing Work Asynchronously

We often need to work asynchronously, and frequently that implies the use of threads.

For example:

  • For the performance of a Web browser to be reasonable, we need to:
    • Download assets (images, JavaScript files, etc.)
    • In parallel across multiple threads.
  • The bottleneck tends to be the network, so we:
    • Parallelize a bunch of requests.
    • Queue up the remainder.
    • And get whatever other work done that we can while we wait for the network to give us our data.

Getting Results on a "Magic Thread"

  • In many environments, one or more threads are special with respect to our background work:
    • In Android, JavaFx, and other GUI environments, updates to the UI are single-threaded, with a specific thread being responsible for those updates.
      • Typically, we have to do substantial work on background threads, so we do not tie up the UI thread and prevent it from updating the UI.
      • So, while the long-running work might need to be done on a background thread, the UI effects of that work need to be done on the UI thread.
    • In Web app development, traditional HTTP verbs (e.g., GET, PUT, POST) are synchronous requests.
      • The Web app forks a thread to respond to a request, and it is up to the code executing on that thread to construct and return the response.
      • Even if that thread delegates work to some other thread (e.g., a processing thread pool).
      • Or process (e.g., a microservice).
      • The Web request thread needs to block waiting for the results (or for some sort of timeout), so that thread can built the appropriate response.

Callbacks

  • Callbacks are simply objects representing chunks of code to be invoked when a certain condition works.
  • In the case of asynchronous operations, the "certain condition" often is:
    • When the operation completes successfully.
    • When the operation completes with an error.
  • In some languages, callbacks could be implemented as anonymous functions or lamda expressions, while in other languages they might need to be implementations of certain interfaces.

Callback hell occurs when you have a lot of nested callbacks, such as this Java snippets:

  • Here, doSomething(), doTheNextThing(), and doSomethingElse() might all arrange to do work on background threads, calling methods on the callbacks when that work completes.
     

enter image description here
 

enter image description here

What Would be Slick

The ugliness comes from the challenges in following the execution flow through layers upon layers of callbacks.

  • Ideally, the syntax for our code would not be inextricably tied to the threading model of our code.
     
  • If doSomething(), doTheNextThing(), and doSomethingELse() could all do their work on the current thread, we could have something more like this:

Java

doSomething();

try {
	String result = doSomethingElse(doTheNextThing());

	// TODO
}
catch(Throwable t) {
	// TODO
}

Kotlin

doSomething()

try {
	val result = doSomethingElse(doTheNextThing())

	// TODO
}
catch(t: Throwable) {
	// TODO
}
  • What we want is to be able to do something like this, while still allowing those functions to do their work on other threads.

Actual Coroutine Syntax

As it turns out, coroutines does just that.

  • If doSomething(), doTheNextThing(), and doTheNextThing() all employ coroutines, our code invoking those functions could look something like this:
someCoroutineScope.launch {
	doSomething()

	try {
		val result = doSomethingElse(doTheNextThing())
	 
		// TODO
	}
	catch(t: Throwable) {
		// TODO
	}
}

Key Pieces of Coroutines

Let's look at the following use of coroutines:

import kotlinx.coroutines.*

fun main() {
	GlobalScope.launch(Dispatchers.Main) {
		println("This is executed before the delay")
		stallForTime()
		println("This is executed after the delay")
	}
	println("This is executed immediately")
}

suspend fun stallFortime() {
	withContext(Dispatchers.Default) {
		delay(2000L)
	}
}
  • If you run this, you will see:
    • "This is executed immediately".
    • "This is executed before the delay".
    • "This is executed after the delay." after a two-second delay.

The Dependencies

While one element of coroutines (the suspend keyword) ios part of the language, the rest comes from libraries.

  • You will need to add these libraries to your project in order to be able to use coroutines.
  • Exactly how you add these libraries will depend a lot on your project type and the build system that you are using:
    • This book will focus on projects built using Gradle.
      • Such as Android projects in Android Studio.
      • Kotlin/ Multiplatform projects in Intellij IDEA.
  • This book focuses mostly on version 1.2.2 and 1.3- RC of these dependencies.

Kotlin/JVM and Android

  • For an Android project, you would want to add a dependency on org.jetbrains.kotlinx:kotlinx-coroutines-android.
  • This has transitive dependencies:
    • To pull in the core coroutine code.
    • Plus it has Android-specific elements.
       
  • If you are using Kotlin/JVM for ordinary Java code, though, the Android code, though, is of no use to you.
  • Instead, add a depedency on org.jetbrains.kotlinx:kotlinx-coroutines-core.

Kotlin/JS

  • You would want to add a dependency on org.jetbrains.kotlinx:kotlinx-coroutines-core-js.
     
  • Overall, Kotlin/JS has the least support for coroutines among the major Kotlin variants.
  • Mostly, that is because JavaScript itself does not offer first-class threads, but instead relies on Promise, web workers, and similar structures.

Kotlin/Native

  • You would want to add a dependency on org.jetbrains.kotlinx:kotlinx-coroutines-core-native.
     
  • Coroutines support for Kotlin/Native, right now, is roughly on par with that of Kotlin/JS.

Kotlin/Common

  • In a Kotlin/Multiplatform project, you can depend upon org.jetbrains.kotlinx:kotlinx-coroutines-core-common in any modules that are adhering to the Kotlin/Common subset.

The Scope

All coroutine work is managed by a CoroutineScope.

  • In the sample code, we are using GlobalScope, which is a global instance of a CoroutineScope.
     
  • Primarily, a CoroutineScope is responsible for:
    • Canceling and cleaning up coroutines when the CoroutineScope is no longer needed.
  • GlobalScope will set up to support the longest practical lifetime: the lifetime of the process that is running the Kotlin code.
     
  • However, while GlobalScope is reasonable for book samples like this, more often you still want to use a scope that is a bit smaller.

For example, if you are using coroutines in an Android app.

  • And you are doing I/O to populate a UI.
  • If the user navigates away from the activity or fragment, you may no longer need that coroutine to be doing its work.
  • This is why Android, through the Jetpack, offers a range of CoroutineScope implementations that will clean up coroutines when they are no longer useful.

The Builder

  • The launch() function that we are calling on GlobalScope is a coroutine builder.
  • Coroutine builder functions take a lamda expression and consider it to be the actual work to be performed by the coroutine.

The Dispatcher

  • Part of the configuration that you can provide to a coroutine builder is a dispatcher.
    • This indicates what thread pool (or similar structure) should be used for executing the code inside of a coroutine.
       
  • Our code snippet refers to two of these:
    • Dispatchers.Default represents a stock pool of threads, useful for general-purpose background work.
    • Dispatchers.Main is a dispatcher that is associated with the "main" thread of the environment, such as Android's main application thread.
       
  • By default, a coroutine builder will use Dispatchers.Default, though that default can be overriden in different circumstances, such as different implementations of CoroutineScope.

The suspend Function

The coroutine builders set up blocks of code to be executed by certain thread pools.

  • Java developers might draw an analogy to handing a Runnable over to some Executor.
  • For Dispatchers.Main, Android developers might draw an analogy to handing a Runnable over to post() on View or runOnUiThread() on Activity, to have the runnable code be executed on that thread.

However, those analogies are not quite complete.

  • In the Runnable scenarios, the unit of work for the designated thread (or thread pool) is the Runnable itself.
  • Once the thread starts executing the code in that Runnable, that thread is now occupied.
  • So, if elsewhere we try handing other Runnable objects over, those will wait until the first Runnable is complete.
     
  • In Kotlin, though, we can mark functions with the suspend keyword.
  • This tells the coroutines system that it is OK to suspend execution of the current block of code and to be feel free to run other code from another coroutine builder if there is any such code to run.

Our stallForTime() function has the suspend keyword.

  • So, when Kotlin starts executing the code that we provided to launch(), when it comes time to call stallForTime(), Kotlin could elect to execute other coroutines scheduled for Dispatchers.main.
  • However, Kotlin will not execute the println() statement on the line after the stallForTime() until stallForTime() returns.

suspend function

  • Any function marked with suspend needs to be called either:
    1. From inside a coroutine builder.
    2. From another function marked with the suspend keyword.
  1. stallForTime() is OK, because we are calling it from code being executed by coroutine builder. [launch()].
  2. delay() is also OK, because stallForTime() has the suspend keyword, so it is safe to call suspend functions like delay() from within stallForTime().

delay() delays for the requested number of milliseconds.

The Context

The dispatcher that we provide to a coroutine builder is part of a CoroutineContext.

  • CoroutineContext: provides a context for executing a coroutine.
    • The dispatcher is also part of that context, but there are elements of a CoroutineContext, such as a Job.

withContext()

  • The withContext() global function is a suspend function, so it can only be executed from inside of another suspend function or from inside of a code block executed by a coroutine builder.
     
  • withContext() takes a different block of code and executes it with a modified CoroutineContext.
     
  • On our snippet, we use withContext() to switch to a different thread pool.
    1. Our coroutine starts executing on the main application thread (Dispatchers.Main).
    2. In stalllForTime(), though, we execute our delay() call on Dipatchers.Default, a courtesy to the withContext() call.
    3. withContext() will block until the code completes, but since it is a suspend function, Kotlin could start work on some other Dispatchers.Main coroutine while waiting for our withContext() call to end.

Suspending main()

You do not need GlobalScope.launch() inside your main() function.

Instead, you can just put the suspend keyword on main() itself.

Now, you can call other suspend functions from main() without having to fuss with the GlobalScope or some other coroutine scope.

  • Little production is used directly from a main() function.
  • And your choice of Coroutine scope and Coroutine builder are fairly important concepts.
import kotlinx.coroutines.*

suspend fun main() {
	println("This is executed before the delay")
	stallForTime()
	println("This is executed after the delay")
}

suspend fun stallForTime() {
	withContext(Dispatchers.Default) {
		delay(2000L)
	}
}

The Timeline for Events

Sequential Statements

  • One coroutine.
  • It triggers two delay() calls (by way of two stallForTime() calls).
import kotlinx.coroutines.*

fun main() {
	GlobalScope.launch(Dispatchers.Main) {
		println("This is executed before the first delay.")
		stallForTime()
		println("This is executed after the first delay.")
		println("This is executed before the second delay.")
		stallForTime()
		println("This is executed after the second delay.")	
	}
	println("This is executed immediately.")
}

suspend fun stallForTime() {
	withContext(Dispatchers.Default) {
		delay(2000L)
	}
}
This is executed immediately 
This is executed before the first delay 
This is executed after the first delay 
This is executed before the second delay 
This is executed after the second delay

Parallel Coroutines

Now, let's divide the work into two separate coroutines, though both are tied to Dispatchers.Main.

  • We enqueue two coroutines.
     
  • Kotlin starts executing the first one, but when it hits the stallForTime() call, it knows that it can suspend execution of that first coroutine, so Kotlin just waits for our first stallForTime() call to complete before proceeding.
  • Now, though, we have two coroutines, so when Kotlin encounters our first stallForTime(), Kotlin can start executing the second coroutine, even though both coroutines are tied to the same thread (Dispatchers.Main).
     
  • So, Kotlin runs the first println() from the second coroutine, then hits the stallForTime().
  • At this point, Kotlin has unblocked coroutine to run, so it waits for one of the suspend functions to complete.
  • It can then resume execution of that coroutine.
import kotlinx.coroutines.*

fun main() {
	GlobalScope.launch(Dispatchers.Main) {
		println("This is executed before the first delay.")
		stallForTime()
		println("This is executed after the first delay")
	}

	GlobalScope.launch(Dispatchers.Main) {
		println("This is executed before the second delay.")
		stallForTime()
		println("This is executed after the second delay.")
	}
	println("This is executed immediately.")
}
	suspend fun stallForTime() {
		withContext(Dispatchers.Default) {
			delay(2000L)
     }
}
This is executed immediately 
This is executed before the first delay 
This is executed before the second delay 
This is executed after the first delay 
This is executed after the second delay

Cheap, Compared to Threads

In thread-centric programming, we worry about creating too many threads.

  1. Each thread consumes a chunk of heap space.
  2. Plus, context-switching between threads consumes CPU time on top of the actual code execution.
    • This is why we have scaling algorithms for sizing thread pools.
    • (e.g., twice the number of CPU cores, plus one).
       
  • However, in Kotlin, coroutines do not declare what thread they run on.
    • They declare what dispatcher they run on.
  • The dispatcher determines the threading rules, which can be:
    • A single thread.
    • Or a constrained thread pool.
import kotlinx.coroutines.*

fun main() {
	for(i in 1..100) {
		GlobalScope.launch(Dispatchers.Main) {
			println("This is executed before delay $i")
			stallForTime()
			println("This is executed after delay $i")
		}
	}
}

suspend fun stallForTime() {
	withContext(Dispatchers.Default) {
		delay(2000L)
	}
}
  • Here, we execute 100 coroutines, each delaying for two seconds.
  • If we were using threads for each of those blocks, we would have 100 threads, which is far too many.
  • Instead, we have as many threads as Dispatchers.Main uses, and Dispatchers.Main uses only one thread.
  • Yet, we can do work on the coroutines somewhat in parallel, with Kotlin switching between them when it encounters suspend functions.

So, our code will wind up printing 100 "before delay" messages before printing any of the "after delay" messages.

As we should be able to print 100 messages before the first two-second delay expires.

The History of Coroutines

While Kotlin may be the most popular use of coroutines today, coroutines themselves have been around as programming construct for quite some time.

  • They fell out of favor for much of that time, though in favor of a thread-based concurrency model.
  • All of this comes back to "multi-tasking".

Preemptive and Cooperative

The original use of the term "multitasking" in programming was in regards to how programs would appear to perform multiple things ("tasks") at once.

  • Two major approaches for multitasking evolved:
    • Cooperative.
    • Preemptive.
Preemptive

Most modern programmers think in terms of preemptive multitasking, as it is the form of multitasking offered by processes and threads.

  • Here, the code that implements a task is oblivious (mostly) to the fact that there is any sort of multitasking going on.
  • Instead, arranging to have tasks run or not is a function of the operating system.
  • The OS schedules processes and threads to run on CPU cores, and the OS switches cores to different processes and threads to ensure that everything that is supposed to be executing gets a chance to do a little bit of work every so often.
Cooperative
  • Here, code needs to explicitly "yield".
    • And indicate that if there is some other task needing CPU time, this would be a good point in which to switch to that task.  
  • Some framework - whether (1) part of an OS or (2) within an individual process - can then switch between tasks at their yield points.
    • 16-bit Windows programs and the original MAC OS used cooperative multitasking at their core.
       
  • Most often nowadays, cooperative multitasking is handled by some framework inside of an process.
  • The OS itself uses preemptive multitasking, to help manage misbehaving processes.
    • Within a process, cooperative multitasking might be used.

For example, 32-bit Windows moved to a preemptive multitasking approach overall, but added fibers as a cooperative multitasking option that could be used within a process.

The Concept of Coroutines

Coroutines originated as the main vehicle for cooperative multitasking.

  • Classic implementations of coroutines might literally use the keyword yield .
    • Or statement to indicate "this is a good time to switch to some other coroutine, if needed".
  • In Kotlin's coroutines, mostly that is handled by calling a suspend function.
     
  • Coroutines, as with any form of cooperative multitasking, requires cooperation.
  • If a coroutine does not yield, then other coroutines cannot run during that period of time.
  • This makes coroutines a poor choice of concurrent model between apps, as each app's developers have a tendency to think that their app is more important than is any other app.
  • However, with an app, developers have to cooperate if their coroutines misbehave otherwise, lest their app crash or encounter other sorts of bugs.

Kotlin's cooroutines are purely an in-app solution for concurrency, and Kotlin relies upon the OS and its preemptive multitasking (processes and threads) for helping to mediate CPU access between several apps.