- You can offload heavy work to background threads but there is a limit to how many background threads you can create so eventually you'll run out of memory:
Using coroutines with memory consumption of one background thread you can perform so many heavy operations and that's what is cool about it:
- Coroutines != Thread
- Coroutines are like light-weight threads
- Like threads, coroutines can run in parallel, wait for each other, and communicate with each other
- Creating coroutines is very cheap - almost free. Create thousands of them without any memory issues
fun main () { // Executes in main thread
println("Main program starts: ${Thread.currentThread().name}")
thread { // creates a background thread (worker thread) using lambda expression
println("Fake work starts: ${Thread.currentThread().name}")
Thread.sleep(1000) // pretend doing some work ... may be file upload
println("Fake work finished: ${Thread.currentThread().name}")
}
println("Main program ends: ${Thread.currentThread().name}")
//application waits for all background threads to complete to exit the process
}
- Unlike threads, for coroutines, the application by default does not wait for it to complete to exit the process
Thread.sleep()
will block the thread and hence also block other coroutines in the thread so we usedelay
to suspend the coroutine- The coroutine equivalent of the above:
fun main () { // Executes in main thread
println("Main program starts: ${Thread.currentThread().name}")
GlobalScope.launch { // creates a background coroutines that runs on a background thread
println("Fake work starts: ${Thread.currentThread().name}")
delay(1000) // Coroutine is suspended but Thread (say T1) is free (not blocked)
println("Fake work finished: ${Thread.currentThread().name}") // Either T1 or some other thread
}
runBlocking { // Creates a coroutine that blocks the current main thread
delay(2000) //application does not wait for all coroutines to exit process so we add a fake wait
}
println("Main program ends: ${Thread.currentThread().name}")
}
The above code can also be written in a cleaner way as:
fun main () { // Executes in main thread
println("Main program starts: ${Thread.currentThread().name}")
runBlocking { // Creates a coroutine that blocks the current main thread
GlobalScope.launch { // Thread: T1
println("Fake work starts: ${Thread.currentThread().name}") // Thread: T1
delay(1000) // Coroutine is suspended but Thread (say T1) is free (not blocked)
println("Fake work finished: ${Thread.currentThread().name}") // Either T1 or some other thread
}
delay(2000) //application does not wait for all coroutines to exit process so we add a fake wait
println("Main program ends: ${Thread.currentThread().name}") // main thread
}
}
or as a runBlocking
function:
fun main() = runBlocking { // Executes in main thread
println("Main program starts: ${Thread.currentThread().name}")
GlobalScope.launch { // creates a background coroutines that runs on a background thread
println("Fake work starts: ${Thread.currentThread().name}")
delay(1000) // Coroutine is suspended but Thread (say T1) is free (not blocked)
println("Fake work finished: ${Thread.currentThread().name}") // Either T1 or some other thread
}
delay(2000) //application does not wait for all coroutines to exit the process so we add a fake wait
println("Main program ends: ${Thread.currentThread().name}")
}
GlobalScope.launch()
is non-blocking in naturerunBlocking()
blocks the thread in which it operates
- A function with a
suspend
modifier is known as a suspending function - The suspending functions are only allowed to be called from a coroutine or from another suspending function
- They cannot be called from outside a coroutine
- 3 most important coroutine builders/creators:
launch
&GlobalScope.launch
,async
&GlobalScope.async
,runBlocking
GlobalScope.launch
(discouraged as if you lose reference to it'll run till the process ends) for things like file download, play music whereas local scope launch for things like some data computation, login operation
local launch
builder (aka fire & forget because you don't block the calling code or wait for the result):
- launches a new coroutine without blocking the current thread
- will inherit coroutine scope and thread of immediate parent coroutine
- returns a reference to
Job
object which you can use for cancellation or wait for the coroutine to finish
fun main() = runBlocking { // Executes in main thread
println("Main program starts: ${Thread.currentThread().name}") // main thread
val job : Job = launch { // local launch will inherit coroutine scope and thread of immediate parent coroutine
println("Fake work starts: ${Thread.currentThread().name}")
delay(1000) // Coroutine is suspended but Thread: main is free (not blocked)
println("Fake work finished: ${Thread.currentThread().name}") // Either main thread or some other thread
}
job.join() //waits for coroutine to finish
println("Main program ends: ${Thread.currentThread().name}") // main thread
}
using async
coroutine builder (non-blocking as well) you can return some data in addition to doing everything that you can do with launch
and Job
fun main() = runBlocking { // Executes in main thread
println("Main program starts: ${Thread.currentThread().name}") // main thread
val jobDeferred : Deferred<String> = async { // local launch will inherit coroutine scope and thread of immediate parent coroutine
println("Fake work starts: ${Thread.currentThread().name}")
delay(1000) // Coroutine is suspended but Thread: main is free (not blocked)
println("Fake work finished: ${Thread.currentThread().name}") // Either main thread or some other thread
"Hello Async"
}
val value : String = jobDeferred.await()
//jobDeferred.join()
println("Main program ends: ${Thread.currentThread().name}") // main thread
}
runBlocking
is generally used to write test cases to test suspending functions
@Test
fun myFirstTest() = runBlocking {
myOwnSuspendingFunc()
Assert.assertEquals(10, 5+5)
}
To make a coroutine cancellable it has to be cooperative:
- Periodically invoke a suspending function that checks if a coroutine was canceled, only those suspending functions that belong to
kotlinx.coroutines
package will make the coroutine cooperative:delay(), yield(), withContext(), withTimeout()
etc. - explicitly check for the cancellation status within the coroutine:
CoroutineScope.isActive
boolean flag (true
when coroutine is active,false
when coroutine is cancelled)
fun main() = runBlocking { //Executes in main thread
println("Main program starts: ${Thread.currentThread().name}") // main thread
val job : Job = launch {
for (i in 0..500){
print("$i.")
delay(50) // or yield() or any other suspending function as per your need
}
}
delay(200) //let's print few values before we cancel
// job.cancel()
// job.join() //cancel but waits for coroutine to finish
job.cancelAndJoin()
println("\nMain program ends: ${Thread.currentThread().name}") // main thread
}
- If you want to execute a suspending function from a
finally
block then wrap the code withinwithContext(NonCancellable)
function - Handle
CancellationException
:
fun main() = runBlocking { // Executes in main thread
println("Main program starts: ${Thread.currentThread().name}") // main thread
val job : Job = launch (Dispatchers.Default) {
try {
for (i in 0..500){
print("$i.")
delay(5) // or yield() or any other suspending function as per your need
}
} catch (ex: CancellationException) { // a suspending function will throw cancellable exception
println("Exception caught safely")
} finally {
println("\nClose resources in finally")
}
}
delay(10) // let's print few values before we cancel
job.cancelAndJoin()
println("\nMain program ends: ${Thread.currentThread().name}") // main thread
}
withTimeout
andwithTimeoutOrNull
and also coroutine builders:
withTimeout(2000) {
try {
for (i in 0..500){
print("$i.")
delay(500)
}
} catch (ex: TimeoutCancellationException) {
// ..code
} finally {
// ..code
}
}
val result: String? = withTimeoutOrNull(2000) {
for (i in 0..500) {
print("$i.")
delay(500)
}
"I am done"
}
- Code execution within coroutine is by default sequential, all methods execute in sequence:
fun main() = runBlocking { //Executes in main thread
println("Main program starts: ${Thread.currentThread().name}") // main thread
val time = measureTimeMillis {
val msgOne = getMessageOne()
val msgTwo = getMessageTwo()
println("The entire message is: ${msgOne + msgTwo}")
}
println("Completed in $time ms")
println("\nMain program ends: ${Thread.currentThread().name}") // main thread
}
suspend fun getMessageOne(): String {
delay(1000L) // pretend to do some work
return "Hello "
}
suspend fun getMessageTwo(): String {
delay(1000L) // pretend to do some work
return "World"
}
- To get concurrent execution you can use
async
orlaunch
as child coroutine builders:
fun main() = runBlocking { //Executes in main thread
println("Main program starts: ${Thread.currentThread().name}") // main thread
val time = measureTimeMillis {
val msgOne : Deferred<String> = async { getMessageOne() } //launch can also be used for concurrency
val msgTwo : Deferred<String> = async { getMessageTwo() }
println("The entire message is: ${msgOne.await() + msgTwo.await()}")
}
println("Completed in $time ms")
println("\nMain program ends: ${Thread.currentThread().name}") // main thread
}
suspend fun getMessageOne(): String {
delay(1000L) // pretend to do some work
return "Hello "
}
suspend fun getMessageTwo(): String {
delay(1000L) // pretend to do some work
return "World"
}
- Only execute the coroutine if the result is used in the code:
fun main() = runBlocking { //Executes in main thread
println("Main program starts: ${Thread.currentThread().name}") // main thread
val time = measureTimeMillis {
val msgOne : Deferred<String> = async (start = CoroutineStart.LAZY) { getMessageOne() }
val msgTwo : Deferred<String> = async (start = CoroutineStart.LAZY) { getMessageTwo() }
// println("The entire message is: ${msgOne.await() + msgTwo.await()}") // uncomment this line to see lazy coroutine execution
}
println("Completed in $time ms")
println("\nMain program ends: ${Thread.currentThread().name}") // main thread
}
suspend fun getMessageOne(): String {
delay(1000L) // pretend to do some work
println("After working in getMessageOne()")
return "Hello "
}
suspend fun getMessageTwo(): String {
delay(1000L) // pretend to do some work
println("After working in getMessageTwo()")
return "World!"
}
- Each coroutine has its own
CoroutineScope
instance attached to it - Each coroutine irrespective of parent or child has its own scope
CoroutineScope
represents what kind of coroutine you have created
fun main() = runBlocking {
println("runBlocking: $this")
launch {
println("launch: $this")
launch {
println("child launch: $this") //different object hash in log
}
}
val job = async {
println("async: $this")
}
}
- Similar to
CoroutineScope
each coroutine has its ownCoroutineContext
(dispatcher, job)- dispatcher determines the thread of a coroutine
CoroutineContext
can be inherited from parent coroutine to child coroutine (read comments):
fun main() = runBlocking { // Thread: main
// this: CoroutineScope instance
// coroutineContext: CoroutineContext instance
/*
Without Parameter: CONFINED [CONFINED DISPATCHER]
- Inherits CoroutineContext from immediate parent coroutine
- Even after delay() or suspending function, it continues to run in the same thread
*/
launch {
println("C1: ${Thread.currentThread().name}") // Thread: main
delay(1000)
println("C1 after delay: ${Thread.currentThread().name}") // Thread: main
}
/*
With parameter: Dispatchers.Default [similar to GlobalScope.launch{}]
- Gets its own context at Global level. Executes in a separate background thread.
- After delay() or suspending function execution, it continues to run either in the same thread or some other thread
*/
launch (Dispatchers.Default) {
println("C2: ${Thread.currentThread().name}") // Thread: T1
delay(1000)
println("C2 after delay: ${Thread.currentThread().name}") // Thread: T1 or some other thread
}
/*
With parameter: Dispatchers.Unconfined [UNCONFINED DISPATCHER]
- Inherits CoroutineContext from the immediate parent coroutine
- After delay() or suspending function execution, it continues to run in some other thread
*/
launch (Dispatchers.Unconfined) {
println("C3: ${Thread.currentThread().name}") // Thread: main
delay(100)
println("C3 after delay: ${Thread.currentThread().name}") //Thread: some other thread T1
//now this is coroutineContext of parent
launch (coroutineContext) {
println("C5: ${Thread.currentThread().name}") // Thread: T1
delay(1000)
println("C5 after delay: ${Thread.currentThread().name}") // Thread: T1
}
}
// Using coroutineContext property to flow context from parent to child
launch (coroutineContext) {
println("C4: ${Thread.currentThread().name}") // Thread: main
delay(1000)
println("C4 after delay: ${Thread.currentThread().name}") // Thread: main
}
println("...Main Program...")
}