Coroutine vs. Job

The doc’s state:

“A launch coroutine builder returns a Job object that is a handle to the launched coroutine”

https://kotlinlang.org/docs/coroutines-basics.html#an-explicit-job

So a Job object is handle to a launched coroutine.

Question: Why does the library include an extra Job abstraction (e.g “a handle”) for tracking the lifecycle of a Coroutine? Why is this extra level of indirection important ? Simply put, why was the launch api designed to return a Job type rather than a Coroutine type which directly expresses the concept at hand ? like so:

fun main() {
runBlocking {
val coroutine : Coroutine = launch {
delay(100)
println(“Task 1 finished”)
}
}

You probably want to read that with a looser interpretation of of the word “handle”.

There’s no extra abstraction. The “Job” interface is directly implemented by internal classes like “StandaloneCoroutine”.

They probably named it “Job” so it’d be easier for devs to understand. Already familiar terms can help when learning new APIs and concepts.

1 Like

There’s no extra abstraction. The “Job” interface is directly implemented by internal classes like “StandaloneCoroutine”

I disagree. An Interface is an abstraction - it defines the general concept. My point is, we already have the name “Coroutine”, I don’t see a need for a “Job” alias.

They probably named it “Job” so it’d be easier for devs to understand. Already familiar terms can help when learning new APIs and concepts.

Actually, this the first time I’ve seen the term Job used in the context of multi-threading/async programing and I found it somewhat confusing when reading about the library (hence my post) as both “Job” and “Coroutine” are often used interchangeably.

I would be happy having Coroutine = “Concurrent Task” without Job in between, unless there is more to it…

May I suggest you check out terms such as job queue and job scheduler?

You’ll also find widespread references to ‘tasks’, which clearly have just about the same meaning in this context;

2 Likes

May I suggest you check out terms such as job queue and job scheduler?

IMHO these are not considered concurrency primitives nor there is such claim in the wiki descriptions.

You’ll also find widespread references to ‘tasks’, which clearly have just about the same meaning in this context;

Call it Task / Coroutine / Goroutine whatever… the issue is not about a “better name” but rather having two names for the same thing without any apparent value.

Job may or may not be backed by an actual coroutine so it’s not quite the same as a “coroutine”. A Deferred also may or may not be backed by a coroutine. CompletableJob is an implementation which has no running coroutine.

2 Likes

Job may or may not be backed by an actual coroutine so it’s not quite the same as a “coroutine”. A Deferred also may or may not be backed by a coroutine

Can you please include code showing a scenario where having a job not backed by a coroutine is useful ?

Well for starters defining a CoroutineScope is defined with a Job instance and a Dispatcher. There’s also things like supervisorJob and nonCancellable jobs that both have special meanings. Simply, Coroutine (which is Continuation<Unit>) is the stdlib primitive for a piece of suspend code, while Job is a kotlinx-coroutines abstraction for a structured-concurency, cancellable piece of work, almost like a Disposable from RxJava.

Well for starters defining a CoroutineScope is defined with a Job instance and a Dispatcher. There’s also things like supervisorJob and nonCancellable jobs that both have special meanings.

If you rename Job to Coroutine in any of these you would get the same semantics. I see no difference between SupervisorJob / “SupervisorCoroutine” as far as defining a parent which does not cancel other child coroutines upon some specific child cancel exception.

Coroutine (which is Continuation<Unit> ) is the stdlib primitive for a piece of suspend code

Thats a possible interpretation but not the definition:

Well again, at its core coroutines are not cancellable, which is a key feature of Jobs, and so that already makes them semantically not equivalent. Also, with a simple interface comparison of Continuation vs Job, there are many, many other differences. For example, a Continuation, at its core, does not have the concept of children, or structured concurrency in any way, while Job has. Also, a Job finishes to its completion, not guaranteeing a value, while a Continuation is resumed with a value or an exception, which are different concepts. In other words, not all continuations are Jobs, and not all Jobs are continuations. Jobs just define that management token that may or may not be related to a Continuation, while a Continuation just defines that thing that’s waiting for a result. Again, emphasising the point that the Job is a management thing, it is part of the CoroutineContext. This separation between Job and Continuation allows the building of abstractions like structured concurrency and other mechanisms on top.

From the docs:

The most basic instances of Job interface are created like this:

  • Coroutine job is created with launch coroutine builder. It runs a specified block of code and completes on completion of this block.
  • CompletableJob is created with a Job() factory function. It is completed by calling CompletableJob.complete.

and so you can have a CompletableJob for example that isn’t related to any Coroutines and only completes when you call complete on it, which is again a different semantic from how Continuations work.

1 Like

Well again, at its core coroutines are not cancellable,

I don’t agree that Coroutine = Continuation, so how do you figure ? where is it written that coroutines are not cancellable?

Again, emphasising the point that the Job is a management thing, it is part of the CoroutineContext

I argue that the ‘management thing’ should be called Coroutine not Job

Coroutine job is created with launch coroutine builder. It runs a specified block of code and completes on completion of this block.

A coroutine builder returning a Coroutine Job doesn’t make any sense to me.

you can have a CompletableJob for example that isn’t related to any Coroutines and only completes when you call complete on it, which is again a different semantic from how Continuations work

I view the term “Coroutine” as describing an async operation not execution mechanics. I’m totally fine with having a CompletableCoroutine. So I think its a matter of personal opinion. I’ll open an issue with JetBrains to get their take on this.

(This ended up being longer than I expected, so take a deep breath and dive in with an open, knowledge-thirsty mind. Oh and all the code snippets are either from the Kotlin stdlib, the Kotlin compiler source code, or kotlinx-coroutines, and all of which are easily available to check validity)
Okay there we go, I think I found the cause of confusion here. I haven’t supported the statement Continuation = Coroutine, which is purely my fault, so sorry for that. Basically, if you look through the standard library, the only way to create a coroutine is with the aptly-named (suspend () -> T).createCoroutine function that is an extension on suspend functions. That function is defined as follows:

public fun <T> (suspend () -> T).createCoroutine(
    completion: Continuation<T>
): Continuation<Unit> =
    SafeContinuation(createCoroutineUnintercepted(completion).intercepted(), COROUTINE_SUSPENDED)

(There’s also startCoroutine that simply creates a coroutine with the above mechanism and calls resume(Unit) on it)
As you can see, this function returns a Continuation<Unit>, which is the only representation in the Kotlin stdlib for a coroutine. It also uses createCoroutineUnintercepted which is platform-specific. On the JVM, for example, it’s defined like this:

public actual fun <T> (suspend () -> T).createCoroutineUnintercepted(
    completion: Continuation<T>
): Continuation<Unit> {
    val probeCompletion = probeCoroutineCreated(completion)
    return if (this is BaseContinuationImpl)
        create(probeCompletion)
    else
        createCoroutineFromSuspendFunction(probeCompletion) {
            (this as Function1<Continuation<T>, Any?>).invoke(it)
        }
}

where createCoroutineFromSuspendFunction is a function defined like this:

private inline fun <T> createCoroutineFromSuspendFunction(
    completion: Continuation<T>,
    crossinline block: (Continuation<T>) -> Any?
): Continuation<Unit> {
// Omitted the implementation for length but it simply returns a ContinuationImpl object

For more evidence, all suspending lambdas are of this following class:

// Suspension lambdas inherit from this class
internal abstract class SuspendLambda(
    public override val arity: Int,
    completion: Continuation<Any?>?
) : ContinuationImpl(completion), FunctionBase<Any?>, SuspendFunction {

which clearly inherits from ContinuationImpl
On the JS side, there’s a CoroutineImpl class that inherits from Continuation too, defined like this:

internal abstract class CoroutineImpl(private val resultContinuation: Continuation<Any?>) : Continuation<Any?>

The Kotlin compiler also makes references to suspend functions being represented as Continuations. For example, one of the transformations performed is described like this in the source code:

::SuspendLambdaLowering,
    "SuspendLambda",
    "Transform suspend lambdas into continuation classes"

and its implementation creates a class extending SuspendLambda (which, if you remember, extends Continuation) and adds an invoke method to it that calls invokeSuspend. It also directly states in a comment:

    // Invoke function in lambdas is responsible for
    //   1) calling `create`
    //   2) starting newly created coroutine by calling `invokeSuspend`.
    // Thus, it creates a clone of suspend lambda and starts it.

invokeSuspend is an abstract method in BaseContinuation that is implemented by the Kotlin compiler, but also you can see it implemented in the return value of createCoroutineFromSuspendFunction like this:

object : ContinuationImpl(completion as Continuation<Any?>, context) {
            private var label = 0

            override fun invokeSuspend(result: Result<Any?>): Any? =
                when (label) {
                    0 -> {
                        label = 1
                        result.getOrThrow() // Rethrow exception if trying to start with exception (will be caught by BaseContinuationImpl.resumeWith
                        block(this) // run the block, may return or suspend
                    }
                    1 -> {
                        label = 2
                        result.getOrThrow() // this is the result if the block had suspended
                    }
                    else -> error("This coroutine had already completed")
                }
        }

and for regular, named suspend functions, there’s a different AddContinuationsLowering that has a method called generateContinuationClassForNamedFunction that does that magic internally to create a class extending ContinuationImpl and then add the needed invokeSuspend method to it.
The kotlinx-coroutines library uses these intrinsic coroutine primitives to create its own coroutines. One example of that is the classic CoroutineScope.launch call, which is defined like this:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

StandaloneCoroutine extends from AbstractCoroutine, which wayyy down the rabbit hole ends up calling (suspend () -> T).startCoroutineCancellable, which (guess what) calls createCoroutineUnintercepted which ends up representing the primitive coroutine as a Continuation.

Simply, all coroutines are represented as Continuation<Unit>, which is widely agreed upon in the Kotlin community too. For example, Arrow-Fx’s documentation mentions that equivalence. ProAndroidDev also seems to have come to the same conclusions:

The compiler internally creates a private class that not only holds the data but also calls the suspend function -in our case syncData()- recursively to resume the execution of the function.

class SyncDataStateMachine(completion: Continuation<Any>) : CoroutineImpl(completion)
As you can see, this class extends from CoroutineImpl . But what is CoroutineImpl? Well, it’s just a subtype of continuation which expects the completion object as a constructor parameter (This is the continuation object which will be used to communicate back to the function)

The invokeSuspend function, which is used to resume the state of the StateMachine, by calling the syncData function with the information of the continuation to trigger the State Machine again.

and so I’m led to strongly believe that the term “Coroutine” absolutely represents execution mechanics

“This ended up being longer than I expected, so take a deep breath and dive in with an open, knowledge-thirsty mind…”

Appreciate your effort ! (Spoiler. I’m still not nconvinced : -)

Okay there we go, I think I found the cause of confusion here. I haven’t supported the statement Continuation = Coroutine, which is purely my fault, so sorry for that.

No worries.

As you can see, this function returns a Continuation, which is the only representation in the Kotlin stdlib for a coroutine.

Says you.

The Kotlin docs says it’s an: “Interface representing a continuation after a suspension point that returns a value of type T.”

Continuation

I say It’s a callback to the code which comes after the Coroutine invocation

In any case, this Interface represents implementation details. It is of minimal concern (if any) to the user of the library.

For more evidence, all suspending lambdas are of this following class …

The Kotlin compiler also makes references to suspend functions being represented as Continuations. For example, one of the transformations performed is described like this in the source code…

and its implementation creates a class extending SuspendLambda (which, if you remember, extends Continuation) and adds an invoke method to it that calls invokeSuspend. It also directly states in a comment…

Of course it makes sense that SuspendLambda is a Continuation but these are there to implement the coroutine concept they do not represent the concept

Simply, all coroutines are represented as Continuation,

Almost. A coroutine (the higher level concept) is implemented via a chain of Continuations.

and so I’m led to strongly believe that the term “Coroutine” absolutely represents execution mechanics

In general, I would avoid defining concepts based on implementation details which are subject to change.

I suggest you check out the Wiki for coroutines at Coroutine - Wikipedia it’s a good read, it does not focus on any specific execution mechanics.

Notice the following in particular:

“an instance of a subroutine only returns once, and does not hold state between invocations. By contrast, coroutines can exit by calling other coroutines, which may later return to the point where they were invoked in the original coroutine; from the coroutine’s point of view, it is not exiting but calling another coroutine”

And see how clearly it appears in Kotlin

val coroutine = launch {
    // This block is the BODY of the coroutine, 
    // Kotlin (very appropriately) calls it the "Coroutine Scope" - it's the execution scope of the coroutine
    delay(200) // exit
    println("Continuing") // return
  }

All I’m saying is that having a type Job seems totally unnecessary to model a Coroutine.

The return type of launch should be named “Coroutine” not “Job” and all is good!

One of the key considerations is the design philosophy that separates the basic compiler infrastructure and minimal standard library support from the full library features (kotlinx.coroutines) that make use of this capability making specific design and implementation choices.

This for example makes it that you can write your own version that works differently, tries some experiment or maintains compatibility with otherwise deprecated functions (kotlinx.coroutines could in principle introduce incompatible versions in parallel without breaking code - it wouldn’t be quite like that as you can easily get dependency conflicts with such a low-level library).

1 Like

Just to sharpen my point:

Force yourself to think that a call to delay() is actually calling a coroutine not a suspend function (implementation details)

val coroutineA =  launch {
      delay(200) // call a coroutine named "delay" and suspend "coroutineA"
      println("Continuing") // resume coroutineA
}

And yes, in this instance, you don’t have access to the “delay” coroutine object - not a big deal.

I don’t really understand, you say that “coroutine” means just an async operation and it doesn’t specify execution mechanics and then you quote Wikipedia that clearly says it does. Coroutines can suspend and resume, they allow to switch context of a thread, they are scheduled cooperatively - this makes them very special and much different than e.g threads. Coroutines and threads could be special cases of a Job.

In fact, it should be possible in the future to create another implementation of Job that would be based on threads. Then the code that needs to do something with jobs or deferred values would work flawlessly with both coroutines and threads. If Job would be named Coroutine, it would become messy, because thread isn’t a special case of a coroutine.

2 Likes

See, despite being abstract, a coroutine is still a ‘thing’ which can be viewed from different perspectives, for example consider a coroutine from a user perspective (e.g from the ‘outside’) then calling a coroutine is like invoking an async operation in a sense that it does not block the calling thread until the coroutine (e.g operation) completes. Alternatively, you can look at a coroutine from within (e.g from the ‘inside’) as series of exits and returns.

With respect to “execution mechanics”, perhaps I should have been more clear sticking with the term “implementation details”. The wiki alludes to how coroutines are implemented in various programing languages but no one specific defines the coroutine abstraction. Anyway, what I was trying to say is that IMO Coroutine = Continuation is problematic because in general continuations represent a chain of code blocks that can span multiple coroutines so the coroutine ‘thing’ gets lost. Perhaps you can view a Coroutine as a suspend function but then how would you expose a coroutine lifecycle api to the caller? So having a dedicated class modeling a couroutine seems like a straight forward solution, then why call it a Job ?

As far as Job, The type in question is part of the coroutine package - its not orthogonal to the package, it does not stand independent without context, nor the wiki describe coroutine in terms of jobs. As it stands it can’t abstract coroutine and something else.

Even from end-user perspective coroutine isn’t just some generic task, but a task with very specific properties, typical for… coroutines. When using coroutines you model solutions to some problems much differently than when using e.g. threads. So this is not only a matter of implementation details.

I’m not sure if this is a proper explanation, we should probably wait for someone from Kotlin team, but I see it like this:

  • job - very abstract - this is some kind of task with at least partial support for cancellations and structured concurrency. It could be a coroutine, it could be a thread, it could be something that only simulates background operation, but it isn’t, it could be almost anything.
  • coroutine - also abstract, it is a concept of routines that could cooperatively jump from one to another. In Kotlin world “coroutine” may mean both the abstract concept and a specific implementation in Kotlin.
  • suspending functions - implementation of coroutines on top of JVM. Suspending functions, continuations, etc. is a mechanism that makes possible to use coroutines on VM that does not support them natively.

And yes, you’re right, for now Job is tightly related to coroutines, it is placed in coroutines package, it extends CoroutineContext.Element, it references coroutines in docs, etc. Still, I believe it is a good thing to introduce an abstract concept of Job and Deferred that could be, but does not have to be backed by a coroutine. Even if not today, then maybe in the future.

2 Likes

It’s just good OO design. The docs say “coroutine builder returns a Job object…”, which is correct. You say “A Job object is a handle to the launched coroutine”, which is incorrect.

Job is an interface that could be implemented in many different ways. One of those ways is with a coroutine, but you are not limited to that way.

1 Like

You forgot to mention the entire sentence from the doc

“A launch coroutine builder returns a Job object that is a handle to the launched coroutine”

https://kotlinlang.org/docs/coroutines-basics.html#an-explicit-job

1 Like