> 1. Why do we still need to explicitly call `c...
# coroutines
k
1. Why do we still need to explicitly call
complete()
on a child
Job
, even when it's structurally connected to a parent via
Job(parent)
?
2. How do the internal implementations differ between using
launch
(which creates an
AbstractCoroutine
) and creating a
Job()
directly, and how do those differences affect structured concurrency and job completion behavior?
Hello, I'm currently studying Kotlin coroutines and came across a situation that led me here with a few questions. While exploring the difference between using
Job()
and
Job(parent: Job?)
, I learned that using a parent job ensures the child is part of the structured coroutine hierarchy. This means that if the parent is cancelled, the child job is also cancelled, maintaining structured concurrency. However, I found something confusing. Even when a child job is properly linked to a parent using
Job(parent)
, we still need to call
complete()
explicitly to mark it as finished. I would have expected the structured relationship to automatically handle this. Is there a specific reason behind this design? Why doesn't the completion state propagate implicitly? Additionally, I noticed that when using
launch
, it internally creates an
AbstractCoroutine
instance, which manages its own job and lifecycle. In contrast, using
Job()
alone does not create a coroutine—only a job object. I'm wondering how this implementation difference impacts job behavior and structured concurrency. If anyone could help clarify the reasoning or intent behind this design, I would really appreciate it. Thanks in advance!
e
did you read https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-completable-job/complete.html?
This function transitions this job into completed state if it was not completed or cancelled yet. However, that if this job has children, then it transitions into completing state and becomes complete once all its children are complete. See Job for details.
k
@ephemient Thanks — I did read the docs, but I’m more curious about the design reasoning. Why do we need to explicitly call
complete()
on a child created with
Job(parent)
, even though it's structurally linked to the parent? Is this to separate manual job management from coroutine builders? Would love to understand the intent.
e
Copy code
coroutineScope {
    launch {
        delay(1000)
        // child will take a while to complete
    }
    // parent is completing, but must not return until the child completes
}
🤗 1
😉 1
s
Although
launch
implements its
Job
as an
AbstractCoroutine
object, conceptually I think it's more helpful to think of the job and the coroutine as two separate things. • A coroutine builder makes a coroutine and a job. The coroutine uses the job to record its outcome: when the coroutine finishes running its code, it marks the job as complete (or at least completing). • The
Job()
function just makes a job with no coroutine. Without an associated coroutine, a job will never be automatically marked as complete; you would always need to complete it manually. To understand why completion doesn't propagate down the job hierarchy in the same way that cancellation does, I find it helpful to think of a job as a communication mechanism between two different parties—a producer (usually the coroutine that owns the job) and a consumer. • The
cancel()
function lets a consumer tell the producer to stop doing work. • The
complete()
function lets the producer tell the consumer that it has finished doing work. To complete a task, you have to wait for that task and all of its subtasks—you can't just decide that the subtasks are complete because the parent task said so. The subtasks are still responsible for doing their own work and determining their own completion. On the other hand, to cancel a task, you have to cancel that task and all of its subtasks—so the child tasks should be cancelled when the parent task is cancelled.
👍 1
💯 1
😍 1
k
Thanks! That’s What I am Looking For! It was very helpful. @Sam