I am just wondering what would be the downsides of having an implicit coroutineScope inside suspend functions. I mean, instead of:
suspend fun scoped() = coroutineScope { coroutines launched here }
one could just write:
suspend fun implicitlyScoped() { coroutines launched here }
suspend functions have access to coroutineContext anyway. Is it so costly to create new scope (job), even if it is not needed?
This need to wrap a lot of things into coroutineScope { … } is a bit tiring:
flow {
coroutineScope {
only here I can use some coroutines to make flow collect from multiple sources in parallel
}
}
1 Like
The first issue with this proposal is that CoroutineScope
is part of the kotlinx-coroutines
library and not in the standard library.
Moreover, to give you an example, I am heavily using coroutines without the kotlinx-coroutines
library and having to construct an unused CoroutineScope
for every call is an unacceptable overhead in my opinion.
4 Likes
Wrapping code in coroutineScope()
means you are explicitly saying this function should suspend until all the child coroutines complete. If you don’t want the function to suspend (maybe this is a utility function that will be called from the calling function multiple times to start multiple coroutines) then your suggestion would break things.
1 Like
Since enclosing function is suspending, it is obvious that we are going to suspend here. And if there are coroutines launched inside it is IMHO is also obvious, that I want to suspend until they all finish.
Main problem may be indeed performance. Maybe a new Scope is not needed, because I have no coroutines launched inside. Maybe I want to just switch context. Or establish a supervisor scope instead of an ordinary one.
However main selling point of coroutines seems to be simplicity + correctness. That is why I am looking, how existing syntax could be simplified.
1 Like
It’s not obvious to me that everyone will want this behavior all the time.
coroutineScope {
for (i in 1..10) {
startSomething(i)
}
}
suspend fun CoroutineScope.startSomething(i: Int) {
delay(i)
launch { ... }
}
It’s contrived, but that’s a function that suspends and also launches a new coroutine, but we want the function to return immediately. The outer coroutineScope()
will wait until all of the child coroutines are finished.
1 Like
Making suspend function an extension on CoroutineScope will grant You an IDE warning already, as it is pretty ambiguous. One should decide, whether he/she wants to suspend until everything is finished or return immediately and continue working.
You can always (and IMHO should) place this delay outside second function, so that second function is not suspending.
fvasco
January 15, 2020, 9:44am
7
circusmagnus:
Since enclosing function is suspending, it is obvious that we are going to suspend here. And if there are coroutines launched inside it is IMHO is also obvious, that I want to suspend until they all finish.
It isn’t so obvious, I wrote a library without using any con kotlinx.coroutine
stuff.
GitLab.com
At the same time, may kotlinx.coroutine
’s methods don’t require a CoroutineScope
.
A coroutine scope is a marker for a fork-join algorithm, using the structured concurrency. You can take a look here:
opened 10:39AM - 27 Jun 18 UTC
closed 07:49AM - 12 Sep 18 UTC
enhancement
design
for 1.0 release
### Background and definitions
Currently coroutine builders like `launch { ..… . }` and `async { ... }` start a **global** coroutine by default. By global we mean that this coroutine's lifetime is completely standalone just like a lifetime of a daemon thread and outlives the lifetime of the job that had started it. It is terminated only explicitly or on shutdown of the VM, so the invoker had to make extra steps (like invoking `join`/`await`/`cancel`) to ensure that it does live indefinitely.
In order to start a **child** coroutine a more explicit and lengthly invocation is needed. Something like `async(coroutineContext) { ... }` and `async(coroutineContext) { ... }` or `async(parent=job) { ... }`, etc. Child coroutine is different from a global coroutine in how its lifetime is scoped. The lifetime of child coroutine is strictly subordinate to the lifetime of its parent job (coroutine). A parent job cannot complete until all its children are complete, thus preventing accidental leaks of running children coroutines outside of parent's scope.
### Problem
**This seems to be a wrong default**. Global coroutines are error-prone. They are easy to leak and they do not represent a typical use-case where some kind of parallel decomposition of work is needed. It is easy to miss the requirement of adding an explicit `coroutineContext` or `parent=job` parameter to start a child coroutine, introducing subtle and hard to debug problems in the code.
Consider the following code that performs parallel loading of two images and returns a combined result (a typical example of parallel decomposition):
```
suspend fun loadAndCombineImage(name1: String, name2: String): Image {
val image1 = async { loadImage(name1) }
val image2 = async { loadImage(name2) }
return combineImages(image1.await(), image2.await())
}
```
This code has a subtle bug in that if loading of the first image fails, then the loading of the second one still proceeds and is not cancelled. Moreover, any error that would occur in the loading of the second image in this case would be lost. Note, that changing `async` to `async(coroutineContext)` does not fully solve the problem as it binds async loading of images to the scope of the larger (enclosing) coroutine which is wrong in this case. In this case we want these async operations to be children of `loadAndCombineImage` operation.
For some additional background reading explaining the problem please see [Notes on structured concurrency, or: Go statement considered harmful](https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/)
### Solution
The proposed solution is to deprecate top-level `async`, `launch`, and other coroutine builders and redefine them as extension functions on `CoroutineScope` interface instead. A dedicated top-level `GlobalScope` instance of `CoroutineScope` is going to be defined.
Starting a global coroutine would become more explicit and lengthly, like `GlobalScope.async { ... }` and `GlobalScope.launch { ... }`, giving an explicit indication to the reader of the code that a global resource was just created and extra care needs to be taken about its potentially unlimited lifetime.
Starting a child coroutine would become less explicit and less verbose. Just using `async { ... }` or `launch { ... }` when `CoroutineScope` is in scope (pun intended) would do it. In particular, it means that the following slide-ware code would not need to use `join` anymore:
```
fun main(args: Array<String>) = runBlocking { // this: CoroutineScope
val jobs = List(100_000) {
launch {
delay(1000)
print(".")
}
}
// no need to join here, as all launched coroutines are children of runBlocking automatically
}
```
For the case of parallel decomposition like `loadAndCombineImage` we would define a separate builder function to capture and encapsulate the current coroutine scope, so that the following code will work properly in all kind of error condition and will properly cancel the loading of the second image when loading of the first one fails:
```
suspend fun loadAndCombineImage(name1: String, name2: String): Image = coroutineScope { // this: CoroutineScope
val image1 = async { loadImage(name1) }
val image2 = async { loadImage(name2) }
combineImages(image1.await(), image2.await())
}
```
### Additional goodies
Another idea behind this design is that it should be easy to turn any entity with life-cycle into an entity that you could start coroutines from. Consider, for example, some kind of an application-specific activity that is launch some coroutines but all of those coroutines should be cancelled when the activity is destroyed (for example). Now it looks like this:
```
class MyActivity {
val job = Job() // create a job as a parent for coroutines
val backgroundContext = ... // somehow inject some context to launch coroutines
val ctx = backgroundContext + job // actual context to use with coroutines
fun doSomethingInBackground() = launch(ctx) { ... }
fun onDestroy() { job.cancel() }
}
```
The proposal is to simply this pattern, by allowing an easy implementation of `CoroutineScope` interface by any business entities like the above activity:
```
class MyActivity : CoroutineScope {
val job = Job() // create a job as a parent for coroutines
val backgroundContext = ... // somehow inject some context to launch coroutines
override val scopeContext = backgroundContext + job // actual context to use with coroutines
fun doSomethingInBackground() = launch { ... } // !!!
fun onDestroy() { job.cancel() }
}
```
Now we don't need to remember to specify the proper context when using `launch` anywhere in the body of `MyActivity` class and all launched coroutines will get cancelled when lifecycle of `MyActivity` terminates.
Yes, I know what coroutineScope is for. I just had a thought, that it could be default / implicit behaviour of any suspend function. So that one would have less boilerplate to write those fork-joins and they would feel more natural.
Nevermind, I realize, that it is not such a good idea in quite a few use cases.
fvasco
January 15, 2020, 11:17am
9
Maybe this is the answer that are you looking for
opened 12:06PM - 21 Feb 18 UTC
closed 12:57PM - 13 Apr 19 UTC
enhancement
All the currently provided channel abstractions in `kotlinx.coroutines` are _hot… _. The data is being produced regardless of the presence of subscriber. This is good for data sources and applications that are inherently hot, like incoming network and UI-events.
However, hot streams are not an ideal solution for cases where data stream is produced on demand. Consider, for example, the following simple code that produces `ReceiveChannel<Int>`:
```
produce<Int> {
while (true) {
val x = computeNextValue()
send(x)
}
}
```
One obvious downside is the `computeNextValue()` is invoked before `send`, so even when receiver is not ready, the next value gets computed. Of course, it gets suspended in `send` if there is no receiver, but it is not as lazy as you get with cold reactive Publisher/Observable/Flowable/Flux/Flow.
We need the abstraction for cold streams in `kotlinx.coroutines` that is going to be just as lazy, computing data in "push" mode versus "pull" mode of hot channels that we have now.
There are the following related discussions:
* https://github.com/reactor/reactor-core/issues/979#issuecomment-351770494 describes preliminary performance test that indicates that "push" mode is much faster for same-thread cases.
* https://github.com/Kotlin/kotlinx.coroutines/issues/113 (SPSC channels) seems to get superseded by the support of cold streams.
1 Like
Yes, I had the same thoughts, that jcornaz was mentioning in referenced issue. Thanks!