This question already has answers here:
Kotlin: Coroutines scope vs Coroutine context
(3 answers)
Closed 6 months ago.
As far as design reasoning goes, why did the authors of Kotlin's coroutine library decide to create CoroutineScope? As far as I can tell, it really only serves as a receiver for coroutine builders that holds a CoroutineContext, as well as some pass through functions to that context's elements. Even a scope's cancellation is just a passthrough call to the context's job.
Did the authors just want to reserve the right to add more things to a scope later? Can't the current iteration of coroutines effectively just cut the scope out and make a CoroutineContext both the receiver and the container for a simpler design?
As Roman Elizarov himself (lead of the coroutines library dev team at the time) described in his article, the difference is their intended purpose.
They effectively contain the same stuff, but they are used in different ways, and giving such interfaces different names allowed to manipulate them more easily and in particular define function signatures more clearly.
Related
After reading some articles I figure out Stackless
means that the coroutines don’t have their own stack, so they don’t map on the native thread
*. so the first point is how it differs from stackfull
and in kotlin, if the coroutine helps us to manage asyn process. so how do these two different coroutines (Stackless and stackfull) work.
because I am unable to find out its function/syntax/scope or any things else in kotlin coroutine. because I am unable to figure out where is mention which scope or function is Stackless or which of stackfull.
It's not important to know the the distinction to be able to use coroutines in Kotlin.
Kotlin coroutines are stackless: You can only call suspend functions from inside other suspend functions.
To boil it down to a very simplistic definition, a stackful coroutine model would let you suspend or yield from anywhere. A stackless model is restricted to suspending only from inside coroutines, but the benefit is that it is lighter weight. Also, it's probably more feasible to retrofit to an existing language. I don't know any details about the Kotlin coroutines implementation under the hood, but I would bet it would be have been near impossible to implement a stackful model and still compile as Java bytecode.
I'd read a lot of the many adventages of using coroutines, but I find nothing about why you shouldn't or couldn't use them.
Why not use all methods as suspend methods, by the way?
I'm having some trouble to understand some concepts here, so with my question I pretend to make the opposite case (why not use it), so I can understand better by contrast.
The main reason not to have all functions suspendable is the overhead they introduce, at least on the JVM. Every suspendable function compiles into a Java method that receives another parameter, the continuation object, and its body compiles into pretty complex state machine code that, among other things, always instantiates another continuation object and daisy-chains it to the one received as the parameter.
So, whenever you have nothing to gain from coroutines, you shouldn't use them as the default way to do things.
Please see my answers inline to your questions:
but I find nothing about why you shouldn't or couldn't use them.
Answer:
a. You should not use them for any foreground task.
b. You should not use them for any simple/real quick operations.
c. You should not use them for any kind of initialization.
Why not use all methods as suspend methods, by the way?
Answer:
a) This will be treated as code smell. Bad practice to do so.
b) If you mark all functions as suspend, then whenever you want to call a suspend function you will have to create a Coroutine Scope to run it.
c) Testing of suspend function is difficult. It needs some additional setup of RunBlockingTest from AndroidX.
The topic of delimited continuations was barely discussed among programming language enthusiasts in the 1990s and 2000s. It has recently been re-emerging as a major thing in programming language discussions.
My hope is that someone can at least authoritatively say whether the continuations underlying Rakudo (as contrasted with Raku) do or don't have each of the six characteristics listed below. I say a bit more about the sort of answer I'm hoping for after the list.
Quoting verbatim (with a formatting touch up) from an online message[1] written by the person driving the work on adding continuations to the JVM:
Asymmetric: When the continuation suspends or yields, the execution returns to the caller (of Continuation.run()). Symmetric continuations don't have the notion of a caller. When they yield, they must specify another continuation to transfer the execution to. Neither symmetric nor asymetric continuations are more powerful than one another, and each could be used to simulate the other.
Stackful: The continuation can be suspended at any depth in the call-stack, rather than in the same subroutine where the delimited context begins when the continuation is stackless (as is the case in C#). I.e the continuation has its own stack rather than just a single subroutine frame. Stackful continuations are more powerful than stackless ones.
Delimited: The continuation captures the execution context that starts with a specific call (in our case, the body of a certain runnable) rather than the entire execution state all the way up to main(). Delimited continuations are strictly more powerful than undelimited ones (http://okmij.org/ftp/continuations/undelimited.html), the latter considered "not practically useful" (http://okmij.org/ftp/continuations/against-callcc.html).
Multi-prompt: Continuations can be nested, and anywhere in the call stack, any of the enclosing continutions can be suspended. This is similar to nesting of try/catch blocks, and throwing an exception of a certain type that unwinds the stack up to the nearest catch that handles it rather than just the nearest catch. An example of nested continuations can be using a Python-like generator inside a virtual thread. The generator code can do a blocking IO call, which will suspend the enclosing thread continuation, and not just the generator: https://youtu.be/9vupFNsND6o?t=2188
One-shot/non-reentrant: Every time we continue a suspended continuation its state is mutated, and we cannot continue it from the same suspension state multiple times (i.e we can't go back in time). This is unlike reentrant continuations where every time we suspend them, a new immutable continuation object that represents a particular suspension point is returned. I.e. the continuation is a single point in time, and every time we continue it we go back to that state. Reentrant continuations are strictly more powerful than non-reentrant ones; i.e. they can do things that are strictly impossible with just one-shot continuations.
Cloneable: If we are able to clone a one-shot continuation we can provide the same ability as reentrant continuations. Even though the continuation is mutated every time we continue it, we can clone its state before continuing to create a snapshot of that point in time that we can return to later.
Aiui continuations aren't directly exposed in Raku, so perhaps the correct answer related to Raku (as against Rakudo) would be "there are no continuations". But that's not clear to me so in the following, in which I describe what I'm hoping might be in an answer if I'm very lucky, I'll pretend it makes some sense to talk about them in the context of both Raku and Rakudo as two distinct realms.
Here's the sort of answer I'm imagining would be possible (though I'm just somewhat wildly guessing at what is actually true):
"As a "100 year" language design, Raku's current underlying semantic [execution?] model requires, at minimum, stackless one-shot multi prompt delimited continuations.
From a theoretic pov, Raku's design can never expand to require that continuations are cloneable but it could theoretically expand to require they are stackful.
Rakudo implements the currently required continuation semantics.
MoarVM has support for these semantics built in, and could realistically track the theoretically possible expansions of requirements if Raku's design ever so expands.
The JVM and JS backends have suitable shims that achieve the same thing, albeit at a cost to performance. It seems plausible that the JVM backend could switch to using continuations that are native to the JVM if it comes to pass that it gets them, provided of course that they meet requirements, but my current impression is that it would likely realistically be perhaps a decade away, or more, before we would need to consider crossing that bridge."
(Or something vaguely like that.)
If an answer also provided a bit more detail on something like the above, perhaps some code links, that would be a particularly awesome addition.
Similarly, if an answer included a couple brief examples of how this continuation power surfaces in current Raku features, and a speculation about how it might one day, say 10 years from now, surface in other features, that would make an answer an over-the-top brilliant one.
PS. Thank you to #Larry who understood things deeply enough to know continuations needed to be part of the picture; to Stefan O'Rear for his contributions, including the initial implementations of what I think are one-shot multi prompt delimited continuations; and to jnthn for making the dream come true.
Footnotes
1 There is work underway to introduce continuations as a first class construct to the JVM. A key driver of this effort is Ron Pressler. The above is based on a message he wrote in November.
Rakudo uses continuations as an implementation strategy for two features:
gather/take - for implementing lazy iterators
Making await on the thread pool non-blocking
The characteristics of the continuations implemented follow the requirements of these language features. I'll go through them in a slightly different order than above because it eases explaining.
Stackful - yes, because we need to be able to do the take or await at any depth in the callstack relative to the gather or the thread pool worker's work loop. For example, you could write a recursive graph traversal algorithm inside of a gather and then take each encountered node. For await, this is at the heart of the difference between Raku's await and await as seen in many other languages: you don't have to refactor all the way up the call stack.
Delimited - yes. The continuation reset operation installs a tag (or "prompt"), and when we do a continuation control operation, we slice the stack at this delimiter. I can't imagine how you'd implement the Raku features involved without them being delimited.
Multi-prompt - yes, this is required because you can be iterating one data source provided by a gather inside of another gather's implementation, or do an await inside of a gather.
Asymmetric - after the continuation has been taken, execution continues after the reset instruction. In the await case, we go and find another task in the worker task queue, and in the take case we're back in the pull-one method of the iterator and can return the taken value. I think this approach fits well in a language where only a few features use continuations.
One-shot/non-reentrant - yes, and at least in MoarVM the memory safety of the runtime depends on this property. It is enforced by an atomic compare and swap operation, so if two threads were to race to invoke the continuation, only one could ever succeed. No Raku features need the additional complexity that reentrant continuations would imply.
Cloneable - no, because no Raku features need it. In theory this isn't too awful to implement in MoarVM in terms of saying "yes, we can do it", but I suspect it raises a lot of questions like "how deep should be clone". If you just cloned all the invocation records and similar, you'd still share Scalar containers, Arrays, etc. between the clones.
As I understand it - though I follow from a distance - the JVM continuations are at least partly aimed at the same design space that the Raku await mechanism is in, and so I'd be surprised if they didn't end up providing what Raku needs. This would clearly simplify compilation of Raku code to the JVM (currently it does the global CPS transform as it does code generation, which curiously turned out simpler than I expected), and it'd almost certainly perform much better too, because the transform required probably obscures quite a few things from the perspective of the JIT compiler.
So far as code goes, you can see the current continuations implementation, which uses the continuation data structure which in turn has various bits of memory management. At the time of writing, these have all been significantly refactored as part of the new callstack representation required by ongoing dispatcher work; those changes do make working with continuations more efficient, but don't change the overall set of operations.
The title states my question.
What is exactly the reason why CoroutineScope.launch and Coroutine.async are just extension functions of CoroutineScope instead oa a member function?
What benefits does it provide?
I am asking because maybe the reason behind this design could be helpful in designing things in the future too.
Thank in advance.
Mostly because with extension functions it is easier to structure your code in multiple modules even if it is represented as one class.
CoroutineScope is actually a really good example of this design pattern. Take a look at CoroutineScope.kt where the interface is declared. There is only basic functionality there (plus operator and cancel())
The two functions you mentioned are defined in Builders.common.kt. If You take a look at the contents of this file, you can see that there are multiple classes which are private, this means they can only be used in this file. This tells you right away that you don't need this these classes for the basic functionality which is designed in CoroutineScope.kt, they are only there for launch {...} and async {...}
So if you have a large class with multiple functionality, it makes sense to break it up in multiple files (=modules).
launch and async are coroutine builders, but they aren't the only ones: look in integration modules for future (and another future), publish, the RxJava 2 builders etc. Obviously those can't be members of CoroutineScope, so why should launch and async be?
In addition, by being extension functions you know they don't rely on any CoroutineScope privates (well, they could rely on internals since they are in the same module).
The kotlinx.coroutines uses structural concurrency approach to make sure all errors are propagated to a parent coroutine. Similarly, a parent coroutine will by default wait for all it's child coroutines to complete.
There is a Job object associated with every coroutine when you do launch or async. It is just easier to use extension functions for that design to make it work implicitly, without a code-writer explicit attention
You may have a look at the more detailed explanation :
https://kotlinlang.org/docs/reference/coroutines/basics.html#structured-concurrency
https://medium.com/#elizarov/structured-concurrency-722d765aa952
CompletableDeferred documentation says
All functions on this interface and on all interfaces derived from it are thread-safe and can be safely invoked from concurrent coroutines without external synchronization.
Is it safe to call these functions outside any coroutine?
For SendChannel<E>, offer and close are not suspend and so they can be called outside coroutines syntactically, but is it actually safe to do so?
If a coroutine is needed, what is the cheapest way to start one: launch(Unconfined)?
It is safe to call offer and close from anywhere. That is what documentation means to say with "are thread-safe" phrase.
One of the reasons these methods are included into channel APIs is to enable integration of coroutines with the regular non-coroutine world that is based on various callbacks and event handlers. You can see the actual example of such integration in this guide on UI programming with coroutines.