When to initiate a new scope in Kotlin - kotlin

I have a blocking rest call in my code, and I would like to run it in a different context than the GlobalScope one (which is the best practice all over) but I do not understand if I also need to create a new scope for that operation as well.
On the one hand, it is said to run also on a different scope than the Global one, and with that, that effectively, scope and context are almost the same things.
I am running in a Spring WebFlux (netty) application and consuming rest calls.
Consider the following code samples:
This one seems to be the "best practice one" (where this blogpost https://medium.com/#elizarov/coroutine-context-and-scope-c8b255d59055#:~:text=The%20difference%20between%20a%20context,is%20in%20their%20intended%20purpose.&text=It%20makes%20the%20scope%20in,it%20in%20the%20function%20signature. specifically says not to use CoroutineScope(Dispatchers.IO).launch
val blockingScope = CoroutineScope(Dispatchers.IO)
suspend fun one() = withContext(blockingScope.coroutineContext) {
val queryParams = arrayOf(
BasicNameValuePair("name", "key"),
)
blockingClient.get("...", JsonNode::class.java, queryParams)
}
As well, do I need to take care of the blockingScope.cancel() here?
But what prevents me from using this all the same (or how will it affect the application)
suspend fun one() = withContext(Dispatchers.IO) {
val queryParams = arrayOf(
BasicNameValuePair("name", "key"),
)
blockingClient.get("...", JsonNode::class.java, queryParams)
}

what prevents me from using this all the same (or how will it affect
the application)?
The two snippets you posted should be pretty much the same in terms of functionality. In the first one, you're creating a scope with Dispatchers.IO as its context and then calling the withContext builder function with the CoroutineContext you just passed to your newly created scope. In the second one, you just pass it directly.
Note however, that if you use the CoroutineScope you created in the first snippet to launch other coroutines, then cancellation or failure of any child coroutines of your scope will lead to cancellation of all other childern (further reference at the kotlinx.coroutines documentation).
As well, do I need to take care of the blockingScope.cancel() here?
If you mean by taking care of blockingScope.cancel() you mean needing to call .cancel() here on your scope, then no, you don't. You call this function on a CoroutineScope to cancel all its child coroutines. So unless that's what you want to do, you shouldn't call this function :)
Remark
As pointed out in the comments to your question, the warning regarding
CoroutineScope(coroutineContext).launch{...}
is because one's accessing the current coroutineContext here to launch another coroutine - so depending on what's the current context, the new coroutine could be launched in one context or another - you wouldn't know just by looking at the function code.

Related

What is the difference between using coroutineScope() and launching a child coroutine and calling join on it?

I am trying to understand the coroutineScope() suspend function in Kotlin and I'm having a hard time understanding the exact purpose of this function.
As per the kotlinlang docs,
This function is designed for parallel decomposition of work. When any
child coroutine in this scope fails, this scope fails and all the rest
of the children are cancelled (for a different behavior see
supervisorScope). This function returns as soon as the given block and
all its children coroutines are completed.
But I feel this behavior can be achieved by launching a child coroutine and calling join on it.
So for example
suspend fun other() {
coroutineScope {
launch { // some task }
async { // some task }
}
}
This can be written as (scope is a reference to the scope created by the parent coroutine)
suspend fun other(scope: CoroutineScope) {
scope.launch {
launch { // some task }
async { // some task }
}.join()
}
Is there any difference between these two approaches since it looks
like they will produce same result and also seem to work in the same fashion?
If not, is coroutineScope merely a way to reduce this
boilerplate code of passing scope from parent coroutine and
calling join on child coroutine?
TLDR
Using CoroutineScope as in the example adds boilerplate code, is more confusing, error-prone and may handle cases like errors and cancellations differently. coroutineScope() is generally preferred in such cases.
Full answer
These two patterns are conceptually different and are used in different cases. Coroutines are all about sequential code and structured concurrency. Sequential means we can write a traditional code that waits in-place, it doesn't use callbacks, etc. and at the same time we don't get a performance hit. Structured concurrency means concurrent tasks have their owners, tasks consists of smaller sub-tasks that are explicit to the framework.
By mixing both above together we get a very easy to use and error-proof concurrency model where in most cases we don't have to launch background jobs and then manage them manually, watch for errors, handle cancellations, etc. We simply fork into sub-tasks and then join them in-place - that's all.
In Kotlin this is represented by suspend functions. Suspend functions are always executed within some context, this context is passed everywhere implicitly and the coroutines framework provides utils to use this context easily. One of the most common patterns is to fork and then join and this is exactly what coroutineScope() does. It creates a scope for launching sub-tasks and we can't leave this scope until all children are successful. We don't have to pass the scope manually, we don't have to join, we don't have to pass errors from children to their siblings and to parent, we don't have to pass cancellations from the parent to children - this is all automatic.
Therefore, suspend functions and coroutineScope() should be the default way of writing concurrent code with coroutines. This approach is easy to write, easy to read and it is error-proof. We can't easily leak a background task, because coroutineScope() won't let us go anywhere. We can't mistakenly ignore errors from background tasks. Etc.
Of course, in some cases we can't use this pattern. Sometimes, we actually would like to only launch a long-running task and return immediately. Sometimes, we don't consider the caller to be the owner of the task. For example, we could have some kind of a service that manages its tasks and we only schedule these tasks, but the service itself owns them. For these cases we can use CoroutineScope.
By using the scope explicitly we can launch tasks in the different context than the current one or from outside of coroutine world. We generally have more control, but at the same time we partially opt-out of the code correctness guarantees I mentioned above. For example, if we forget to invoke join() we can easily leak background tasks or perform operations in unexpected order. Also, in your case if the coroutine invoking other() is cancelled, all launched operations will be still running in the background. For these reasons, we should use CoroutineScope explicitly only if needed.
Common patterns
As a result of all that was said above, when working with coroutines we usually use one of these patterns:
Suspend function - it runs within the caller context and it waits for all its subtasks, it doesn't launch anything in the background.
Function receiving CoroutineScope either as a param or receiver - usually, that means the function wants to do something with the context even after returning (because otherwise it could be simply a suspend function). It either launches some background tasks or stores the context somewhere for a later use.
Regular function that uses its own CoroutineScope to launch tasks. Usually, this is some kind of a service that keeps its custom context.
At least to me, function which is suspend and receives CoroutineScope is pretty confusing, it is not entirely clear what to expect from it. Will it execute the operation in the caller context or in the provided one? Will it wait to finish or only schedule the operation in the background and return immediately? Maybe it will do both: first do some initial processing synchronously (therefore suspend), but also schedule additional task in the background (therefore scope: CoroutineScope)? We don't know this, we have to read the documentation or source code to understand its behavior. Your second example is unnecessary complication over a simple suspend function.
To further make my point consider this example:
data class User(
val firstName: String,
val lastName: String,
) {
fun getFullName(user: User) = ...
}
This example is far from perfect, but the main point is that it is confusing why we have to pass user to getFullName() if we call this function on a user already. We don't know whether it returns a full name of the passed user, the user we invoked the function on or maybe some kind of a mix? If that would be a member function not receiving a User or a static utility function receiving a User, everything would be clear. But a member function receiving a User is simply confusing. This is similar to your second example where we pass the context both implicitly and explicitly and we don't know which one is used and how exactly.

Why can `runBlocking` be invoked without providing a CoroutineContext, if there's no default value for it?

I always check the implementation of the things I use.
Currently I'm using an injection library that doesn't support suspensable functions (Koin), so, only (even if discouraged) for bootstrapping the app, I'm using runBlocking some times.
In order to have richer logs, I'm enriching the coroutine context with some info, yet that info is lost in most context changes (launch, async, and runBlocking among others).
Specially, given the fact that non-suspend methods don't have access to a CoroutineContext, I'm super curious where does runBlocking gets it from.
You can use runBlocking like:
runBlocking {...}
Yet, when I check its implementation, it has two parameters: a CoroutineContext and the suspend block to be executed. None of those parameters have default value, so why can I call it without passing it? I really don't get it!
Additionally, the page says that the default value is EmptyCoroutineContext but the code docs say something about an event loop.
So I ask again? Why can I call it without passing a value, and what's the actual default?
By default runBlocking() starts with an empty coroutine context.
The fact that the context doesn't have a default value is indeed confusing and strange. I think (but I'm not 100% sure) this is because by ctrl+clicking on runBlocking() we go the implementation, so actual definition. But the code is compiled against the expect declaration. I didn't find an easy way to see expect declaration directly in IntelliJ, but it can be found here:
public expect fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T
As we can see, context in this declaration has a default value. Still, this is really confusing when using IntelliJ.
And regarding the mentioned event loop: yes, runBlocking() creates (or re-uses) an event loop, but I don't see how it relates to the coroutine context.
Remember that the context you pass to a coroutine builder like runBlocking or launch is not the context that will actually become available inside the coroutine, but its parent. The builder adds its own items and merges them into the context you provided.
runBlocking uses its own dispatcher, which exists only during the lifetime of the runBlocking function call. You can find this dispatcher in the coroutine context available inside the body of runBlocking, for example using this code:
import kotlinx.coroutines.runBlocking
fun main() {
runBlocking {
val ctxElems = coroutineContext.fold(mutableListOf<Pair<Any, Any>>()) { list, element ->
list.also { it.add(element.key to element) }
}
for ((key, value) in ctxElems) {
println("${key::class.qualifiedName}: $value")
}
}
}
This prints
kotlinx.coroutines.CoroutineId.Key: CoroutineId(1)
kotlinx.coroutines.Job.Key: "coroutine#1":BlockingCoroutine{Active}#12843fce
kotlin.coroutines.ContinuationInterceptor.Key: BlockingEventLoop#3dd3bcd
(coroutine dispatchers belong to the broader category of continuation interceptors)
The other part of your question, why you don't have to pass in the seemingly non-default parameter, is answered elsewhere. Basically, it's an artifact of the IDE and the expected vs. actual declarations. The relevant declaration is the expected one, and actual is an implementation detail, but the IDE takes you to that one.

Is it OK to use redundant/nested withContext calls?

I have a personal project written in Kotlin, and I developed a habit of using withContext(...) very generously. I tend to use withContext(Dispatchers.IO) when calling anything that could possibly be related to I/O.
For example:
suspend fun getSomethingFromDatabase(db: AppDatabase) = withContext(Dispatchers.IO) {
return // ...
}
suspend fun doSomethingWithDatabaseItem(db: AppDatabase) {
val item = withContext(Dispatchers.IO) {
getSomethingFromDatabase(db)
}
// ...
}
You can see a redundant withContext(Dispatchers.IO) in the second function. I'm being extra cautious here, because I might not know/remember if getSomethingFromDatabase switches to an appropriate context or not. Does this impact performance? Is this bad? What's the idiomatic way of dealing with Dispatchers?
Note: I know that it's perfectly fine to switch between different contexts this way, but this question is specifically about using the same context.
You do not need withContext for anything besides calling code that demands a specific context. Therefore withContext(Dispatchers.Main) should only be used when you're working with UI functions that require the main thread. And you should only use withContext(Dispatchers.IO) when calling blocking IO related code.
A proper suspend function does not block (see Suspending convention section here), and therefore, you should never have to specify a dispatcher to call a suspend function. The exception would be if you're working with someone else's code or API and they are using suspend functions incorrectly!
I don't know what your AppDatabase class is, but if it is sensibly designed, it will expose suspend functions instead of blocking functions, so you should not need withContext to retrieve values from it. But if it does expose blocking functions for retrieving items, then the code of your first function is correct.
And your second function definitely doesn't need withContext because it's simply using it to call something that I can see is a suspend function.
As for whether it's OK to use redundant context switching...it doesn't hurt anything besides possibly wasting a tiny bit of time and memory context switching and allocating lambdas for no reason. And it makes your code less readable.

Should a library function be suspend or return deferred

Let's assume I'm writing a library that returns a string which is a complex and long running task.
I can chose between offering this
interface StringGenerator {
suspend fun generateString(): String
}
or
interface StringGenerator {
fun generateString(): Deferred<String>
}
Are there any (dis-)advantages of either of the options and which are they? Which should I choose?
Kotlin coroutines are designed along the "sequential by default" guideline. That means that your API should always expose suspend funs and the user, if and when they really need it, can easily wrap them in async.
The advantage of that is analogous to the advantages of cold flows with respect to hot flows: a suspendable function is active only while control is inside it. When it returns, it has not left behind a task running in the background.
Whenever you return a Deferred, the user must start worrying what happens if they don't manage to await on the result. Some code paths may ignore it, the calling code may get an exception, and then their application has a leak.

Whats the concept behind a CoroutineScope?

After reading the introduction and the javadoc of CoroutineScope I'm still a little confused what the idea behind a CoroutineScope is.
The first sentence of the doc "Defines a scope for new coroutines." is not clear to me: Why do my coroutines need a scope?
Also, why are standalone coroutine builders deprecated? Why is it better to do this:
fun CoroutineScope.produceSquares(): ReceiveChannel<Int> = produce {
for (x in 1..5) send(x * x)
}
instead of
fun produceSquares(): ReceiveChannel<Int> = produce { //no longer an extension function
for (x in 1..5) send(x * x)
}
You can still use global "standalone" coroutines by spawning them in GlobalScope:
GlobalScope.launch {
println("I'm running unstructured")
}
However, it's not recommended to do this since creating coroutines on a global scope is basically the same we did with good old threads. You create them but somehow need to keep track of a reference to later join/cancel them.
Using structured concurrency, that is nesting coroutines in their scopes, you will have a more maintainable system overall. For example, if you spawn a coroutine inside another one, you inherit the outer scope. This has multiple advantages. If you cancel the outer coroutine, the cancellation will be delegated to its inner coroutines. Also, you can be sure that the outer coroutine will not complete before all its children coroutines have done their work.
There's also a very good example shown in the documentation for CoroutineScope.
CoroutineScope should be implemented on entities with well-defined lifecycle that are responsible for launching children coroutines. Example of such entity on Android is Activity.
After all, the first version of your shown produceSquares methods is better as it is only executable if invoked in a CoroutineScope. That means you can run it inside any other coroutine:
launch {
produceSquares()
}
The coroutine created inside produceSquares inherits the scope of launch. You can be sure that launch does not complete before produceSquares. Also, if you cancelled launch, this would also effect produceSquares.
Furthermore, you can still create a globally running coroutine like this:
GlobalScope.produceSquares()
But, as mentioned, that's not the best option in most cases.
I'd also like to promote an article I wrote. There are some examples demonstrating what scopes mean: https://kotlinexpertise.com/kotlin-coroutines-concurrency/
It is related to the concept of structured concurrency, which defines a structure between coroutines.
On a more philosophical level, you rarely launch coroutines “globally”, like you do with threads. Coroutines are always related to some local scope in your application, which is an entity with a limited life-time, like a UI element. So, with structured concurrency we now require that launch is invoked in a CoroutineScope, which is an interface implemented by your life-time limited objects (like UI elements or their corresponding view models).
As an evident consequence of this concept: by cancelling the context of a scope, all it's subcoroutines will be canceled, too.