Skip to content

Commit 3e387b8

Browse files
committed
Job.children property is introduced;
Job.cancelChildren is now an extension (member is deprecated & hidden); Job.joinChildren extension is introduced.
1 parent 4cf9dd2 commit 3e387b8

File tree

5 files changed

+90
-17
lines changed

5 files changed

+90
-17
lines changed

core/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/Deferred.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import kotlin.coroutines.experimental.CoroutineContext
4848
* _cancelling_ state immediately. A simple implementation of deferred -- [CompletableDeferred],
4949
* that is not backed by a coroutine, does not have a _cancelling_ state, but becomes _cancelled_
5050
* on [cancel] immediately. Coroutines, on the other hand, become _cancelled_ only when they finish
51-
* executing their code and after all their children complete.
51+
* executing their code and after all their [children] complete.
5252
*
5353
* ```
5454
* wait children
@@ -71,7 +71,7 @@ import kotlin.coroutines.experimental.CoroutineContext
7171
* or the cancellation cause inside the coroutine.
7272
*
7373
* A deferred value can have a _parent_ job. A deferred value with a parent is cancelled when its parent is
74-
* cancelled or completes. Parent waits for all its children to complete in _completing_ or
74+
* cancelled or completes. Parent waits for all its [children] to complete in _completing_ or
7575
* _cancelling_ state. _Completing_ state is purely internal. For an outside observer a _completing_
7676
* deferred is still active, while internally it is waiting for its children.
7777
*

core/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/Job.kt

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,15 @@ import kotlinx.coroutines.experimental.selects.select
3030
import java.util.concurrent.Future
3131
import kotlin.coroutines.experimental.Continuation
3232
import kotlin.coroutines.experimental.CoroutineContext
33+
import kotlin.coroutines.experimental.buildSequence
3334
import kotlin.coroutines.experimental.intrinsics.suspendCoroutineOrReturn
3435

3536
// --------------- core job interfaces ---------------
3637

3738
/**
3839
* A background job. Conceptually, a job is a cancellable thing with a simple life-cycle that
3940
* culminates in its completion. Jobs can be arranged into parent-child hierarchies where cancellation
40-
* or completion of parent immediately cancels all its children.
41+
* or completion of parent immediately cancels all its [children].
4142
*
4243
* The most basic instances of [Job] are created with [launch] coroutine builder or with a
4344
* `Job()` factory function. Other coroutine builders and primitives like
@@ -60,7 +61,7 @@ import kotlin.coroutines.experimental.intrinsics.suspendCoroutineOrReturn
6061
*
6162
* A job can be _cancelled_ at any time with [cancel] function that forces it to transition to
6263
* _cancelling_ state immediately. Job that is not backed by a coroutine and does not have
63-
* children becomes _cancelled_ on [cancel] immediately.
64+
* [children] becomes _cancelled_ on [cancel] immediately.
6465
* Otherwise, job becomes _cancelled_ when it finishes executing its code and
6566
* when all its children [complete][isCompleted].
6667
*
@@ -117,15 +118,15 @@ public interface Job : CoroutineContext.Element {
117118

118119
/**
119120
* Returns `true` when this job is active -- it was already started and has not completed or cancelled yet.
120-
* The job that is waiting for its children to complete is still considered to be active if it
121+
* The job that is waiting for its [children] to complete is still considered to be active if it
121122
* was not cancelled.
122123
*/
123124
public val isActive: Boolean
124125

125126
/**
126127
* Returns `true` when this job has completed for any reason. A job that was cancelled and has
127128
* finished its execution is also considered complete. Job becomes complete only after
128-
* all its children complete.
129+
* all its [children] complete.
129130
*/
130131
public val isCompleted: Boolean
131132

@@ -180,6 +181,25 @@ public interface Job : CoroutineContext.Element {
180181

181182
// ------------ parent-child ------------
182183

184+
/**
185+
* Returns a sequence of this job's children.
186+
*
187+
* A job becomes a child of this job when it is constructed with this job in its
188+
* [CoroutineContext] or using an explicit `parent` parameter.
189+
*
190+
* A parent-child relation has the following effect:
191+
*
192+
* * Cancellation of parent with [cancel] or its exceptional completion (failure)
193+
* immediately cancels all its children.
194+
* * Parent cannot complete until all its children are complete. Parent waits for all its children to
195+
* complete in _completing_ or _cancelling_ state.
196+
* * Uncaught exception in a child, by default, cancels parent. In particular, this applies to
197+
* children created with [launch] coroutine builder. Note, that [async] and other future-like
198+
* coroutine builders do not have uncaught exceptions by definition, since all their exceptions are
199+
* caught and are encapsulated in their result.
200+
*/
201+
public val children: Sequence<Job>
202+
183203
/**
184204
* Attaches child job so that this job becomes its parent and
185205
* returns a handle that should be used to detach it.
@@ -205,7 +225,9 @@ public interface Job : CoroutineContext.Element {
205225
/**
206226
* Cancels all children jobs of this coroutine with the given [cause]. Unlike [cancel],
207227
* the state of this job itself is not affected.
228+
* @suppress **Deprecated**: Binary compatibility, it is an extension now
208229
*/
230+
@Deprecated(message = "Binary compatibility, it is an extension now", level = DeprecationLevel.HIDDEN)
209231
public fun cancelChildren(cause: Throwable? = null)
210232

211233
// ------------ state waiting ------------
@@ -385,6 +407,8 @@ public class JobCancellationException(
385407
(message!!.hashCode() * 31 + job.hashCode()) * 31 + (cause?.hashCode() ?: 0)
386408
}
387409

410+
// -------------------- Job extensions --------------------
411+
388412
/**
389413
* Unregisters a specified [registration] when this job is complete.
390414
*
@@ -440,6 +464,25 @@ public suspend fun Job.cancelAndJoin() {
440464
return join()
441465
}
442466

467+
/**
468+
* Cancels all [children][Job.children] jobs of this coroutine with the given [cause] using [cancel]
469+
* for all of them. Unlike [cancel] on this job as a whole, the state of this job itself is not affected.
470+
*/
471+
public fun Job.cancelChildren(cause: Throwable? = null) {
472+
children.forEach { it.cancel(cause) }
473+
}
474+
475+
/**
476+
* Suspends coroutine until all [children][Job.children] of this job are complete using
477+
* [join] for all of them. Unlike [join] on this job as a whole, it does not wait until
478+
* this job is complete.
479+
*/
480+
public suspend fun Job.joinChildren() {
481+
children.forEach { it.join() }
482+
}
483+
484+
// -------------------- CoroutineContext extensions --------------------
485+
443486
/**
444487
* Cancels [Job] of this context with an optional cancellation [cause]. The result is `true` if the job was
445488
* cancelled as a result of this invocation and `false` if there is no job in the context or if it was already
@@ -1057,19 +1100,21 @@ public open class JobSupport(active: Boolean) : Job, SelectClause0, SelectClause
10571100
}
10581101
}
10591102

1060-
override fun attachChild(child: Job): DisposableHandle =
1061-
invokeOnCompletion(onCancelling = true, handler = Child(this, child))
1062-
1063-
public override fun cancelChildren(cause: Throwable?) {
1064-
val state = this.state
1103+
override val children: Sequence<Job> get() = buildSequence<Job> {
1104+
val state = this@JobSupport.state
10651105
when (state) {
1066-
is Child -> state.childJob.cancel(cause)
1067-
is Incomplete -> state.list?.cancelChildrenList(cause)
1106+
is Child -> yield(state.childJob)
1107+
is Incomplete -> state.list?.let { list ->
1108+
list.forEach<Child> { yield(it.childJob) }
1109+
}
10681110
}
10691111
}
10701112

1071-
private fun NodeList.cancelChildrenList(cause: Throwable?) {
1072-
forEach<Child> { it.childJob.cancel(cause) }
1113+
override fun attachChild(child: Job): DisposableHandle =
1114+
invokeOnCompletion(onCancelling = true, handler = Child(this, child))
1115+
1116+
public override fun cancelChildren(cause: Throwable?) {
1117+
this.cancelChildren(cause) // use extension function
10731118
}
10741119

10751120
/**

core/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/NonCancellable.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,15 @@ object NonCancellable : AbstractCoroutineContextElement(Job), Job {
7272
/** Always returns `false`. */
7373
override fun cancel(cause: Throwable?): Boolean = false
7474

75+
/** Always returns [emptySequence]. */
76+
override val children: Sequence<Job>
77+
get() = emptySequence()
78+
7579
/** Always returns [NonDisposableHandle] and does not do anything. */
80+
@Suppress("OverridingDeprecatedMember")
7681
override fun attachChild(child: Job): DisposableHandle = NonDisposableHandle
7782

7883
/** Does not do anything. */
84+
@Suppress("OverridingDeprecatedMember")
7985
override fun cancelChildren(cause: Throwable?) {}
8086
}

core/kotlinx-coroutines-core/src/test/kotlin/kotlinx/coroutines/experimental/CoroutinesTest.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,5 +359,26 @@ class CoroutinesTest : TestBase() {
359359
parent.cancelAndJoin() // cancel parent, make sure no stack overflow
360360
}
361361

362+
@Test
363+
fun testCancelAndJoinChildren() = runTest {
364+
expect(1)
365+
val parent = Job()
366+
launch(coroutineContext, CoroutineStart.UNDISPATCHED, parent = parent) {
367+
expect(2)
368+
try {
369+
yield() // to be cancelled
370+
} finally {
371+
expect(5)
372+
}
373+
expectUnreached()
374+
}
375+
expect(3)
376+
parent.cancelChildren()
377+
expect(4)
378+
parent.joinChildren() // will yield to child
379+
check(parent.isActive) // make sure it did not cancel parent
380+
finish(6)
381+
}
382+
362383
private fun throwIOException() { throw IOException() }
363384
}

coroutines-guide.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1425,7 +1425,8 @@ The following example prints the first ten prime numbers,
14251425
running the whole pipeline in the context of the main thread. Since all the coroutines are launched as
14261426
children of the main [runBlocking] coroutine in its [coroutineContext][CoroutineScope.coroutineContext],
14271427
we don't have to keep an explicit list of all the coroutine we have started.
1428-
We use [cancelChildren] extension function to cancel all the children coroutines.
1428+
We use [cancelChildren][kotlin.coroutines.experimental.CoroutineContext.cancelChildren]
1429+
extension function to cancel all the children coroutines.
14291430

14301431
```kotlin
14311432
fun main(args: Array<String>) = runBlocking<Unit> {
@@ -2371,7 +2372,7 @@ Channel was closed
23712372
[newCoroutineContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/new-coroutine-context.html
23722373
[CoroutineName]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/-coroutine-name/index.html
23732374
[Job()]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/-job.html
2374-
[cancelChildren]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/kotlin.coroutines.experimental.-coroutine-context/cancel-children.html
2375+
[kotlin.coroutines.experimental.CoroutineContext.cancelChildren]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/kotlin.coroutines.experimental.-coroutine-context/cancel-children.html
23752376
[CompletableDeferred]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/-completable-deferred/index.html
23762377
[Deferred.onAwait]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/-deferred/on-await.html
23772378
<!--- INDEX kotlinx.coroutines.experimental.sync -->

0 commit comments

Comments
 (0)