Coroutines explanation - android

read a lot about this but still there are some unclear things.
The sentence: "coroutines enable writing asynchronous code in synchronous way" is a little bit confusing.
Coroutines are ALWAYS asynchronus. they are basically running on the threads all the time? It is on us to decide which threads will coroutines we declare use (Mainscope- they run on MainThread, CoroutinesScope - it will run on some different threads that are not associated with UI thread??, ViewmodelScope - they will run on the threads that are associated with viewmodel??)
Did I get this right?

The traditional way most java world write asynchronous is by using "callbacks", which they think and was the only way to write asynchronous tasks by adding listeners for success and failures.
But coroutines way is slightly different. Actually you call a suspend function like delay(ms: Long) and it doesn't seem asynchronous because there isn't any callbacks involved here, but behind the hood the Continuation object would work in place of callbacks to resume it later when needed.
Coroutines are ALWAYS asynchronus
Yes, they are!
They are basically running on the threads all the time?
Yes, they are!
It is on us to decide which threads will coroutines we declare use
Yep, absolutely. They implement Structured Concurrency.
Mainscope - they run on MainThread
Apparantly, yes.
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
It creates a CoroutineScope with Context's dispatcher element as Dispatchers.Main to make the children dispatch into the main thread by default (when not specified), and attaches a SupervisorJob to make other children do not being affected by cancellation of one.
CoroutinesScope - it will run on some different threads that are not associated with UI thread??
No, it is upto you to provide the CoroutineDispatcher element to the CoroutineContext that will decide which thread it need to dispatch child coroutines to, for example there are 4 default CoroutineDispatcher:
Dispatchers.Main - Dispatches to main thread (single-threaded)
Dispatchers.Default - Dispatches to CommonPool (max thread-count same as cores in your processor)
Dispatchers.IO - Dispatches to CommonPool (max thread-count: 64)
Dispatchers.Unconfined - Dispatches to any thread registered (usually for very small tasks, and is not recommended to be used anyway)
Moreover you can make your own CoroutineDispatcher using java.util.Executors:
Executors.newFixedThreadPool(4).asCoroutineDispatcher() // Your own dispatcher max of 4-threads
There are many other elements in CoroutineContext other than Dispatcher. Like: Job, CoroutineName, CoroutineInterceptor, CoroutineExceptionHandler, etc. I'd recommend reading out this article for better visualization of what's happening with some of the most important Elements of CoroutineContext.
A CoroutineScope is just a wrapper for the CoroutineContext which essentially helps in launching a coroutine using launch or async, you can create one by passing a CoroutineContext as a parameter like: CoroutineScope(Dispatchers.Default + Job() + CoroutineName("My Coroutine")).
Note: All the elements of the CoroutineContext is itself a CoroutineContext, the + sign is overload of plus function defined in CoroutineContext essentially overriding elements from left CoroutineContext with elements from right CoroutineContext (if element does not exist in left it'll just add it).
ViewmodelScope - they will run on the threads that are associated with viewmodel??
ViewModelScope implements Dispatchers.Main by default to launch tasks and they can change UI elements as only main thread can change the UI elements.
SideNote: You can change element of CoroutineContext before launching (overriding one of scope).
Example:
viewModelScope.launch(Dispatchers.Default) {
// This coroutine is launched under Dispatchers.Default (backed by a CommonPool of threads) instead of Dispatchers.Main
// but will follow cancellation as soon as lifecycle of Activity associated is destroyed as Job element is not overriden
}

Related

What happens using a Dispatcher v/s Main Thread in Android

In the context of Android, What is the difference between a dispatcher vs main thread.
As per my understanding referring the documentation,
It is backed by a shared pool of threads on JVM. By default, the
maximal level of parallelism used by this dispatcher is equal to the
number of CPU cores, but is at least two. Level of parallelism X
guarantees that no more than X tasks can be executed in this
dispatcher in parallel.
Will it spawn a new thread or there will be as per the log name DefaultDispatcher-worker-1 a worker that will communicate with the pool of threads other than the main to handle a block of co-routine or worker itself is a Co-routine?
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val timeInMillis = measureTimeMillis {
GlobalScope.launch(Dispatchers.Default) {
Log.d(TAG, "Starting coroutine in thread ${Thread.currentThread().name}")
val answer = doNetworkCall()
withContext(Dispatchers.Main) {
Log.d(TAG, "Setting text in thread ${Thread.currentThread().name}")
}
}
}
Log.d(TAG, "(The operation took $timeInMillis ms)")
}
A Dispatcher is essentially a thread pool. When you use launch(Dispatchers.Default), the Thread used to run the code in the coroutine will be obtained from Dispatchers.Default. Each time there is a suspend function call in the coroutine, when the coroutine resumes after that call, it might be resuming on a different Thread instance coming from the same Dispatchers.Default thread pool.
"DefaultDispatcher-worker-1" is the name of a literal Thread instance that came from the Dispatcher.Default's pool.
withContext is itself a suspend function call, so any code after the withContext block will also be resumed on some thread from Dispatchers.Default. (There is none in your example).
The code inside withContext(Dispatchers.Main) will be run on a thread from Dispatchers.Main.
Dispatchers.Main is a special Dispatcher that has only one thread, and that thread is the same Main Thread used by the OS.
You should rarely ever need to use GlobalScope and it is discouraged because it doesn't allow easy management of coroutine lifecycles. If you use lifecycleScope instead, your coroutines will be automatically cancelled when the associated Activity or Fragment is shut down. This is usually what you want, because an Activity or Fragment shouldn't be continuing to do work after it's gone.
Usually on Android, most coroutines should be launched from lifecycleScope or viewModelScope and should not need to have a Dispatcher specified since these scopes by default use Dispatchers.Main which is usually what you want. (Actually they use a different dispatcher called Dispatchers.Main.immediate which also uses the main thread but also can run the first part of a coroutine immediately without deferring to the next frame of the main thread loop. Not a distinction you need to worry about.) You can wrap the pieces of your coroutine that need other dispatchers in withContext. You don't need to do this if you are only calling suspend functions. By convention it is up to suspend functions to internally delegate to a specific Dispatcher if they need to.
An exception to the above paragraph might be if you're launching a coroutine in viewModelScope that does some blocking work and never touches anything that is main-thread-only. Then you can skip withContext and specify a dispatcher explicitly with launch.

MainScope vs GlobalScope

What is the difference between GlobalScope and MainScope?
//Accessing data from Room
GlobalScope.launch {
v.tvStoreName.text = pfViewModel.getStoreName()
pageDetails.pageNumber = currentPage
pageDetails.pageSize = pageSize
pfViewModel.getTransactions(pageDetails, toolbarBuilder?.getDate()!!)
}
The GlobalScope sometimes makes an error which is very hard to reproduce.
Fatal Exception:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the
original thread that created a view hierarchy can touch its views.
MainScope().launch {
var storeName = ""
withContext(Dispatchers.Default) {
storeName = pfViewModel.getStoreName()
}
v.tvStoreName.text = storeName
}
What is the difference between GlobalScope and MainScope?
MainScope is a CoroutineScope that uses the Dispatchers.Main dispatcher by default, which is bound to the main UI thread.
The GlobalScope is a CoroutineScope that has no dispatcher in its coroutine context. This means that the coroutines launched in this scope will use the Dispatchers.Default dispatcher, which is backed by a thread pool (sized based on the number of CPU cores you have).
The GlobalScope also has no Job in its context, which means structured concurrency doesn't apply. Coroutines launched in it are never cancelled automatically, so they need to be manually controlled. This is why it is usually discouraged to use it unless you have very specific needs.
Only the original thread that created a view hierarchy can touch its views.
This error occurs when you're trying to modify views from outside the main thread, which is what happens if you do this from coroutines launched in the GlobalScope (because it's backed by a separate thread pool).
In your second snippet, you are using withContext(Dispatchers.Default), which only makes this part of the code run on that thread pool, but the rest runs on UI thread. This is why the update of the UI is ok there.
Note that Room already uses a dispatcher with a background thread pool for its queries, so you don't need to switch context manually like this, you can just call it from the UI thread.
Side note: using MainScope().launch { .. } like this is a bad idea, because it suffers from the same cancellation issue as GlobalScope. To use it properly, you would need to extract this scope into a variable/property so you can cancel it when appropriate. That said, it's easier to use an existing scope. Android already provides a ready-to-use coroutine scope in components such as Activities which have a lifecycle (see lifecycle-runtime-ktx library). It's called lifecycleScope. You should launch your coroutines in this scope so they automatically get cancelled when the activity is destroyed.

Kotlin Coroutine GlobalScope documentation clarity

I'm having a hard time understanding the usages and documentation for GlobalScope.
The docs states:
A global CoroutineScope not bound to any job.
Global scope is used to launch top-level coroutines which are
operating on the whole application lifetime and are not cancelled
prematurely. Another use of the global scope is operators running in
Dispatchers.Unconfined, which don’t have any job associated with them.
Application code usually should use an application-defined
CoroutineScope. Using async or launch on the instance of GlobalScope
is highly discouraged.
What does it mean when GlobalScope is not bound to any job? Because I can do
val job = GlobalScope.launch {
// some work
}
job.cancel()
It says they are not canceled prematurely. What does this mean? As you can see above I can cancel it.
Lastly it says, it operates on the whole application lifetime. So the scope stays alive until the application dies. How does this compare with CoroutineScope? When I exit an Android Activity in the middle of a running CoroutineScope, it'll still be alive and runs until completion. Does it just mean the CoroutineScope will get cleaned up w/ garbage collection after it completes and GlobalScope wont?
link to docs:
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/
What does it mean when GlobalScope is not bound to any job? Because I can do...
This refers to the lack of Job object associated to the CoroutineScope, not individual coroutine Jobs within the "Scope".
It says they are not canceled prematurely. What does this mean? As you can see above I can cancel it.
Calling GlobalScope.cancel() at any point will throw an IllegalArgumentException.
so this would produce the same error :
fun main(args: Array<String>) {
val myScope : CoroutineScope = object : CoroutineScope {
override val coroutineContext: CoroutineContext = Dispatchers.IO // no job added i.e + SupervisorJob()
}
val job = myScope.launch { longRunningTask() }
Thread.sleep(5_000)
job.cancel() // this is fine - individual coroutine cancelled
myScope.cancel() // this will throw an error, same as GlobalScope.cancel()
}
Lastly it says, it operates on the whole application lifetime. So the scope stays alive until the application dies. How does this compare with CoroutineScope? When I exit an Android Activity in the middle of a running CoroutineScope, it'll still be alive and runs until completion. Does it just mean the CoroutineScope will get cleaned up w/ garbage collection after it completes and GlobalScope wont?
GlobalScope is an object class - a singleton, thus lasts as long as the process it runs in. GlobalScope implements CoroutineScope its just a predefined scope that comes with the library. Coroutines can be launched from GlobalScope but the caller must take care to retain the Job object and cancel each coroutine via the Job. When using custom CoroutineScope you can have greater control on what happens when you call the extension function CoroutineScope.cancel() - for instance cancelling all child coroutines "running" in that scope.
I'm no authority on the subject but that's how I interpet the documentation and observed behaviours of the library.
If I'm honest I think most people struggle to explain coroutines simply and concisely - most articles on the subject are not easy reading and usually I come away more confused. Having delved into the library source code a bit after it usually demistifies things somewhat, but you can also see it is still very much in a state of continuing development.

GlobalScope vs CoroutineScope vs lifecycleScope

I am used to working with AsyncTask and understand it pretty well due to its simplicity. But Coroutines are confusing to me. Can you please explain to me in a simple way what is the difference and purpose of each of the following?
GlobalScope.launch(Dispatchers.IO) {}
GlobalScope.launch{}
CoroutineScope(Dispatchers.IO).launch{}
lifecycleScope.launch(Dispatchers.IO){}
lifecycleScope.launch{}
First, let's start with definitions to make it clear. If you need a tutorial or playground for Coroutines and Coroutines Flow you can check out this tutorial/playground i created.
Scope is object you use to launch coroutines that only contains one object which is CoroutineContext
public interface CoroutineScope {
/**
* The context of this scope.
* Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope.
* Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages.
*
* By convention, should contain an instance of a [job][Job] to enforce structured concurrency.
*/
public val coroutineContext: CoroutineContext
}
The coroutine context is a set of rules and configurations that define
how the coroutine will be executed.
Under the hood, it’s a kind of map, with a set of possible keys and values.
Coroutine context is immutable, but you can add elements to a context using plus operator,
just like you add elements to a set, producing a new context instance
The set of elements that define the behavior of a coroutine are:
CoroutineDispatcher — dispatches work to the appropriate thread.
Job — controls the lifecycle of the coroutine.
CoroutineName — name of the coroutine, useful for debugging.
CoroutineExceptionHandler — handles uncaught exceptions
Dispatchers
Dispatchers determine which thread pool should be used. Dispatchers class is also
CoroutineContext which can be added to CoroutineContext
Dispatchers.Default: CPU-intensive work, such as sorting large lists, doing complex calculations and similar. A shared pool of threads on the JVM backs it.
Dispatchers.IO: networking or reading and writing from files.
In short – any input and output, as the name states
Dispatchers.Main: mandatory dispatcher for performing UI-related events in Android's main or UI thread.
For example, showing lists in a RecyclerView, updating Views and so on.
You can check out Android's official documents for more info on dispatchers.
Edit Even though official document states that
Dispatchers.IO - This dispatcher is optimized to perform disk or
network I/O outside of the main thread. Examples include using the
Room component, reading from or writing to files, and running any
network operations.
Answer from Marko Topolnic
IO runs the coroutine on a special, flexible thread pool. It exists
only as a workaround when you are forced to use a legacy, blocking IO
API that would block its calling thread.
might be right either.
Job A coroutine itself is represented by a Job.
A Job is a handle to a coroutine. For every coroutine that you create (by launch or async),
it returns a Job instance that uniquely identifies the coroutine and manages its lifecycle.
You can also pass a Job to a CoroutineScope to keep a handle on its lifecycle.
It is responsible for coroutine’s lifecycle, cancellation, and parent-child relations.
A current job can be retrieved from a current coroutine’s context:
A Job can go through a set of states: New, Active, Completing, Completed, Cancelling and Cancelled.
while we don’t have access to the states themselves,
we can access properties of a Job: isActive, isCancelled and isCompleted.
CoroutineScope It is defined a simple factory function that takes CoroutineContexts as arguments to create wrapper around the combined CoroutineContext as
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
ContextScope(if (context[Job] != null) context else context + Job())
internal class ContextScope(context: CoroutineContext) : CoroutineScope {
override val coroutineContext: CoroutineContext = context
// CoroutineScope is used intentionally for user-friendly representation
override fun toString(): String = "CoroutineScope(coroutineContext=$coroutineContext)"
}
and creates a Job element if the provide context does not have one already.
Let's look at GlobalScope source code
/**
* A global [CoroutineScope] not bound to any job.
*
* Global scope is used to launch top-level coroutines which are operating on the whole application lifetime
* and are not cancelled prematurely.
* Another use of the global scope is operators running in [Dispatchers.Unconfined], which don't have any job associated with them.
*
* Application code usually should use an application-defined [CoroutineScope]. Using
* [async][CoroutineScope.async] or [launch][CoroutineScope.launch]
* on the instance of [GlobalScope] is highly discouraged.
*
* Usage of this interface may look like this:
*
* ```
* fun ReceiveChannel<Int>.sqrt(): ReceiveChannel<Double> = GlobalScope.produce(Dispatchers.Unconfined) {
* for (number in this) {
* send(Math.sqrt(number))
* }
* }
* ```
*/
public object GlobalScope : CoroutineScope {
/**
* Returns [EmptyCoroutineContext].
*/
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}
As you can see it extends CoroutineScope
1- GlobalScope.launch(Dispatchers.IO) {} GlobalScope is alive as long as you app is alive, if you doing some counting for instance in this scope and rotate your device it will continue the task/process.
GlobalScope.launch(Dispatchers.IO) {}
runs as long as your app is alive but in IO thread because of using Dispatchers.IO
2- GlobalScope.launch{} It's same as the first one but by default, if you don't have any context, launch uses EmptyCoroutineContext which uses Dispatchers.Default, so only difference is thread with first one.
3- CoroutineScope(Dispatchers.IO).launch{} This one is the same as first one with only syntax difference.
4- lifecycleScope.launch(Dispatchers.IO){} lifecycleScope is an extention for LifeCycleOwner and bound to Actvity or Fragment's lifCycle where scope is canceled when that Activity or Fragment is destroyed.
/**
* [CoroutineScope] tied to this [LifecycleOwner]'s [Lifecycle].
*
* This scope will be cancelled when the [Lifecycle] is destroyed.
*
* This scope is bound to
* [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate].
*/
val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
get() = lifecycle.coroutineScope
You can also use this as
class Activity3CoroutineLifecycle : AppCompatActivity(), CoroutineScope {
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main + CoroutineName("🙄 Activity Scope") + CoroutineExceptionHandler { coroutineContext, throwable ->
println("🤬 Exception $throwable in context:$coroutineContext")
}
private val dataBinding by lazy {
Activity3CoroutineLifecycleBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(dataBinding.root)
job = Job()
dataBinding. button.setOnClickListener {
// This scope lives as long as Application is alive
GlobalScope.launch {
for (i in 0..300) {
println("🤪 Global Progress: $i in thread: ${Thread.currentThread().name}, scope: $this")
delay(300)
}
}
// This scope is canceled whenever this Activity's onDestroy method is called
launch {
for (i in 0..300) {
println("😍 Activity Scope Progress: $i in thread: ${Thread.currentThread().name}, scope: $this")
withContext(Dispatchers.Main) {
dataBinding.tvResult.text = "😍 Activity Scope Progress: $i in thread: ${Thread.currentThread().name}, scope: $this"
}
delay(300)
}
}
}
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
}
TL;DR
GlobalScope.launch(Dispatchers.IO): Launches a top-level coroutine on Dispatchers.IO. Coroutine is unbound and keeps running until finished or cancelled. Often discouraged since programmer has to maintain a reference to join() or cancel().
GlobalScope.launch: Same as above, but GlobalScope uses Dispatchers.Default if not specified. Often discouraged.
CoroutineScope(Dispatchers.IO).launch: Creates a coroutine scope which uses Dispatchers.IO unless a dispatcher is specified in the coroutine builder i.e. launch
CoroutineScope(Dispatchers.IO).launch(Dispatchers.Main): Bonus one. Uses the same coroutine scope as above (if the scope instance is same!) but overrides Dispatcher.IO with Dispatchers.Main for this coroutine.
lifecycleScope.launch(Dispatchers.IO): Launches a coroutine within the lifecycleScope provided by AndroidX. Coroutine gets cancelled as soon as lifecycle is invalidated (i.e. user navigates away from a fragment). Uses Dispatchers.IO as thread pool.
lifecycleScope.launch: Same as above, but uses Dispatchers.Main if not specified.
Explanation
Coroutine scope promotes structured concurrency, whereby you can launch multiple coroutines in the same scope and cancel the scope (which in turn cancels all the coroutines within that scope) if the need be. On the contrary, a GlobalScope coroutine is akin to a thread, where you need to keep a reference in-order to join() or cancel() it. Here's an excellent article by Roman Elizarov on Medium.
CoroutineDispatcher tells the coroutine builder (in our case launch {}) as to which pool of threads is to be used. There are a few predefined Dispatchers available.
Dispatchers.Default - Uses a thread pool equivalent to number of CPU cores. Should be used for CPU bound workload.
Dispatchers.IO - Uses a pool of 64 threads. Ideal for IO bound workload, where the thread is usually waiting; maybe for network request or disk read/write.
Dispatchers.Main (Android only): Uses main thread to execute the coroutines. Ideal for updating UI elements.
Example
I've written a small demo fragment with 6 functions corresponding to the above 6 scenarios. If you run the below fragment on an Android device; open the fragment and then leave the fragment; you'll notice that only the GlobalScope coroutines are still alive. Lifecycle coroutines are cancelled by lifecycleScope when the lifecycle is invalid. On the other hand, CoroutineScope ones are cancelled on onPause() invocation which is explicitly done by us.
class DemoFragment : Fragment() {
private val coroutineScope = CoroutineScope(Dispatchers.IO)
init {
printGlobalScopeWithIO()
printGlobalScope()
printCoroutineScope()
printCoroutineScopeWithMain()
printLifecycleScope()
printLifecycleScopeWithIO()
}
override fun onPause() {
super.onPause()
coroutineScope.cancel()
}
private fun printGlobalScopeWithIO() = GlobalScope.launch(Dispatchers.IO) {
while (isActive) {
delay(1000)
Log.d("CoroutineDemo", "[GlobalScope-IO] I'm alive on thread ${Thread.currentThread().name}!")
}
}
private fun printGlobalScope() = GlobalScope.launch {
while (isActive) {
delay(1000)
Log.d("CoroutineDemo", "[GlobalScope] I'm alive on ${Thread.currentThread().name}!")
}
}
private fun printCoroutineScope() = coroutineScope.launch {
while (isActive) {
delay(1000)
Log.d("CoroutineDemo", "[CoroutineScope] I'm alive on ${Thread.currentThread().name}!")
}
Log.d("CoroutineDemo", "[CoroutineScope] I'm exiting!")
}
private fun printCoroutineScopeWithMain() = coroutineScope.launch(Dispatchers.Main) {
while (isActive) {
delay(1000)
Log.d("CoroutineDemo", "[CoroutineScope-Main] I'm alive on ${Thread.currentThread().name}!")
}
Log.d("CoroutineDemo", "[CoroutineScope-Main] I'm exiting!")
}
private fun printLifecycleScopeWithIO() = lifecycleScope.launch(Dispatchers.IO) {
while (isActive) {
delay(1000)
Log.d("CoroutineDemo", "[LifecycleScope-IO] I'm alive on ${Thread.currentThread().name}!")
}
Log.d("CoroutineDemo", "[LifecycleScope-IO] I'm exiting!")
}
private fun printLifecycleScope() = lifecycleScope.launch {
while (isActive) {
delay(1000)
Log.d("CoroutineDemo", "[LifecycleScope] I'm alive on ${Thread.currentThread().name}!")
}
Log.d("CoroutineDemo", "[LifecycleScope] I'm exiting!")
}
}
I'd organize your list along three axes:
GlobalScope vs. CoroutineScope() vs. lifecycleScope
Dispatchers.IO vs. inherited (implicit) dispatcher
Specify the dispatcher in the scope vs. as an argument to launch
1. Choice of Scope
A big part of Kotlin's take on coroutines is structured concurrency, which means all the coroutines are organized into a hierarchy that follows their dependencies.
If you're launching some background work, we assume you expect its results to appear at some point while the current "unit of work" is still active, i.e., the user hasn't navigated away from it and doesn't care anymore about its result.
On Android, you have the lifecycleScope at your disposal that automatically follows the user's navigation across UI activities, so you should use it as the parent of background work whose results will become visible to the user.
You may also have some fire-and-forget work, that you just need to finish eventually but the user doesn't await its result. For this you should use Android's WorkManager or similar features that can safely go on even if the user switches to another application. These are usually tasks that synchronize your local state with the state kept on the server side.
In this picture, GlobalScope is basically an escape hatch from structured concurrency. It allows you to satisfy the form of supplying a scope, but defeats all the mechanisms it's supposed to implement. GlobalScope can never be cancelled and it has no parent.
Writing CoroutineScope(...).launch is just wrong because you create a scope object without a parent that you immediately forget, and thus have no way of cancelling it. It's similar to using GlobalScope but even more hacky.
2. Choice of Dispatcher
The coroutine dispatcher decides which threads your coroutine may run on. On Android, there are three dispatchers you should care about:
Main runs everything on the single GUI thread. It should be your main choice.
IO runs the coroutine on a special, flexible thread pool. It exists only as a workaround when you are forced to use a legacy, blocking IO API that would block its calling thread.
Default also uses a thread pool, but of fixed size, equal to the number of CPU cores. Use it for computation-intensive work that would take long enough to cause a glitch in the GUI (for example, image compression/decompression).
3. Where to Specify the Dispatcher
First, you should be aware of the dispatcher specified in the coroutine scope you're using. GlobalScope doesn't specify any, so the general default is in effect, the Default dispatcher. lifecycleScope specifies the Main dispatcher.
We already explained that you shouldn't create ad-hoc scopes using the CoroutineScope constructor, so the proper place to specify an explicit dispatcher is as a parameter to launch.
In technical detail, when you write someScope.launch(someDispatcher), the someDispatcher argument is actually a full-fledged coroutine context object which happens to have a single element, the dispatcher. The coroutine you're launching creates a new context for itself by combining the one in the coroutine scope and the one you supply as a parameter. On top of that, it creates a fresh Job for itself and adds it to the context. The job is a child of the one inherited in the context.
You should know that if you want to launch suspend function you need to do it in CoroutineScope. Every CoroutineScope have CoroutineContext. Where CoroutineContext is a map that can contain Dispatcher (dispatches work to the appropriate thread), Job
(controls the lifecycle of the coroutine), CoroutineExceptionHandler (handles uncaught exceptions), CoroutineName (name of the coroutine, useful for debugging).
GlobalScope.launch(Dispatchers.IO) {} - GlobalScope.launch creates global coroutines and using for operations that should not be canceled, but a better alternative would be creating a custom scope in the Application class, and inject it to the class that needs it. This has the advantage of giving you the ability to use CoroutineExceptionHandler or replace the CoroutineDispatcher for testing.
GlobalScope.launch{} - same as GlobalScope.launch(Dispatchers.IO) {} but runs coroutines on Dispatchers.Default.Dispatchers.Default is a default Dispatcher that is used if no dispatchers is specified in their context.
CoroutineScope(Dispatchers.IO).launch{} - it's create scope with one parameter and launch new coroutine in it on IO thread. Will be destroyed with object where it was launched. But you should manually call .cancel() for CoroutineScope if you want to end your work properly.
lifecycleScope.launch(Dispatchers.IO){} - it is existing scopes that available from a Lifecycle or from a LifecycleOwner (Activity or Fragment) and comes in your project with dependency androidx.lifecycle:lifecycle-runtime-ktx:*. Using it you can rid from manualy creating CoroutineScope. It will run your job in Dispatchers.IO without blocking MainThread, and be sure that your jobs will be cancelled when your lifecycle is destroyed.
lifecycleScope.launch{} - same as lifecycleScope.launch(Dispatchers.IO){} that create CoroutinesScope for you with default Dispatchers.Main parameter and runs your coroutines in Dispatcher.Main that mean you can work with UI.

Do suspend functions execute in another thread and come back with the result?

I've looked at plenty of articles online but I'm still a bit confused as to what happens specifically, step by step, when suspend functions are suspended in coroutines.
I know that a suspend function, under the hood, is just a regular function with a Continuation parameter that allows it to resume, but my confusion is regarding where that suspend function or coroutine goes and where it comes back once resumed.
I've heard a few people saying "they don't necessarily come back to the same thread" and I don't get it, can someone please explain this to me step by step?
TLDR;
There is no guarantee, it may or may not,
it really depends that on the following points:
Is the dispatcher is multi-threaded?
Is there any dispatcher override in between?
LONG Answer
A coroutine has a CoroutineContext that specify how it behaves, where it run.
A CoroutineContext is mainly build up with four elements: Job, CoroutineName, CoroutineExceptionHandler and Dispatcher.
Its responsibility of the dispatcher to dispatch the coroutine. A dispatcher can be paused to stop coroutines to even run (this is useful in unit testing) mentioned here in the android conference talk, it may be a single-threaded dispatcher just like Dispatchers.Main, it has an event-loop like javascript has.
So, it really depends that on the following points:
Is the dispatcher is multi-threaded?
For example: This will run on single thread.
suspend fun main() {
val dispatcherScope = CoroutineScope(Executors.newSingleThreadExecutor().asCoroutineDispatcher())
val job = dispatcherScope.launch {
repeat(10) {
launch {
println("I'm working in thread ${Thread.currentThread().name}")
// every coroutine on same thread
}
}
}
job.join()
}
Run it here, other single threaded dispatchers: Dispatchers.Main
Is there any dispatcher override in between?
With the same context, if we override the dispatcher before launch, it will change the thread even if original context is based on single-threaded event-loop, each coroutine will run on different thread creating 10 different thread:
dispatcherScope.launch {
repeat(10) {
launch(Dispatchers.IO) {
println("I'm working in thread ${Thread.currentThread().name}")
// every coroutine on same thread
}
}
}
Run it here, other multi-threaded Dispatchers: Dispatchers.Default, Executor based dispatcher, Dispatchers.Unconfined (this launch coroutine in any free thread).
The short answer is: they execute somewhere and come back with the result to somewhere.
The long(er) explanation for the short answer:
"Somewhere" might be the same thread, it might be a different thread - it depends on the dispatcher and in many cases, the current state of the dispatcher. For instance, the contents of a SequenceScope will (by default) run on the same thread.
Another case where a suspend function might run on the same thread is if it's using the same dispatcher as the calling function.
Dispatchers can also share threads between them to save on thread creation (and just keep a count of the maximum number of parallel operations for their own tasks), so even switching between different dispatchers that each use thread pools may not result in using a different thread.
As far as people saying "they don't necessarily come back to the same thread," this is correct, for similar reasons. Keep in mind that your dispatcher may have many threads, and the one that you were using before you got suspended might be occupied with a different function right now, so the dispatcher will simply pick a different thread to run your code on.
When the coroutine suspends, the underlying Java method returns a special COROUTINE_SUSPENDED value. If the calling function is also suspendable, it also returns the object, and so the execution returns to the innermost plain, non-suspendable function. This function typically runs an event loop, where event handlers are calls to continuation.resume(). So now it is ready to take the next handler from the queue and resume another coroutine.
When you call continuation.resume(), the continuation itself knows about the dispatcher in charge of the coroutine and delegates to it. If the current thread isn't owned by that dispatcher, it dispatches an event to another event loop, the one served by the dispatcher's thread pool. This way the dispatcher controls the thread where the corutine resumes.

Categories

Resources