Skip to content

Commit 10c099e

Browse files
committed
Add a documentation for JobSupport.addLastAtomic
* Document internal invariants * Add memory footprint test * This explanation can be used to reasonably review follow-up PR that gets rid of addLastIf
1 parent fd5a58b commit 10c099e

File tree

2 files changed

+69
-10
lines changed

2 files changed

+69
-10
lines changed

kotlinx-coroutines-core/common/src/JobSupport.kt

+44-8
Original file line numberDiff line numberDiff line change
@@ -464,8 +464,9 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
464464
if (state.isActive) {
465465
// try move to SINGLE state
466466
if (_state.compareAndSet(state, node)) return node
467-
} else
467+
} else {
468468
promoteEmptyToNodeList(state) // that way we can add listener for non-active coroutine
469+
}
469470
}
470471
is Incomplete -> {
471472
val list = state.list
@@ -522,7 +523,32 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
522523
return node
523524
}
524525

525-
private fun addLastAtomic(expect: Any, list: NodeList, node: JobNode) =
526+
/**
527+
* Adds the [node] to the given [list] if the state of this job is equal to [expect].
528+
*
529+
* This method is invoked only from [invokeOnCompletion] (two places) and [expect] is always a snapshot of the
530+
* `state` that [invokeOnCompletion] works on.
531+
*
532+
* It is required to preserve the following invariants:
533+
*
534+
* 1) Exactly once handler invocation.
535+
* Without atomic addIf, the following scenario is possible:
536+
* * T1 reads `state`, attempts to add a handler to the list, gets preempted
537+
* * T2 finalizes the state, CASes it and invokes all the handlers from the list
538+
* * T1 adds its handler to the list to be never invoked
539+
* If T1 starts to re-check the state after the addition, then the handler might be invoked twice.
540+
*
541+
* 2) Parent finalization.
542+
*
543+
* NB: it's totally unobvious from the signature, but [tryWaitForChildren] if's over `invokeOnCompletion` result:
544+
* if it's `NonDisposableHandle` then the addition failed, otherwise it's successfully added.
545+
* If handler fails to be added, then child is cancelled/completed and there is no need to
546+
* either wait for it **or** invoke its handler.
547+
*
548+
* Additional implementation note: all internal handlers implement [LockFreeLinkedListNode] and
549+
* LFLL **does not** support additions and removals of the same node (with the same identity) more than once.
550+
*/
551+
private fun addLastAtomic(expect: Any, list: NodeList, node: JobNode): Boolean =
526552
list.addLastIf(node) { this.state === expect }
527553

528554
private fun promoteEmptyToNodeList(state: Empty) {
@@ -904,7 +930,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
904930
notifyRootCause?.let { notifyCancelling(list, it) }
905931
// now wait for children
906932
val child = firstChild(state)
907-
if (child != null && tryWaitForChild(finishing, child, proposedUpdate))
933+
if (child != null && tryWaitForChildren(finishing, child, proposedUpdate))
908934
return COMPLETING_WAITING_CHILDREN
909935
// otherwise -- we have not children left (all were already cancelled?)
910936
return finalizeFinishingState(finishing, proposedUpdate)
@@ -916,16 +942,26 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
916942
private fun firstChild(state: Incomplete) =
917943
state as? ChildHandleNode ?: state.list?.nextChild()
918944

919-
// return false when there is no more incomplete children to wait
920-
// ## IMPORTANT INVARIANT: Only one thread can be concurrently invoking this method.
921-
private tailrec fun tryWaitForChild(state: Finishing, child: ChildHandleNode, proposedUpdate: Any?): Boolean {
945+
/**
946+
* This method is invoked by the JobSupport lifecycle when it transitions into "completing" state (also includes "cancelling"),
947+
* and in order to be transitioned into its final state it has to wait for all the children.
948+
* If this method returns `true` then the current job is waiting for its children.
949+
* It's done via an addition of a special completion handler to the child that, when invoked, invokes
950+
* [continueCompleting] that almost immediately fallbacks to [tryWaitForChildren].
951+
*
952+
* If it returns `false` then there is no children to wait for and it's safe to finalize the final job's state.
953+
* Note that at this point, new children can appear:
954+
* * For `cancelling` state they are immediately cancelled.
955+
* * For `completing` state they are not cancelled and left hanging out to dry.
956+
*/
957+
private tailrec fun tryWaitForChildren(state: Finishing, child: ChildHandleNode, proposedUpdate: Any?): Boolean {
922958
val handle = child.childJob.invokeOnCompletion(
923959
invokeImmediately = false,
924960
handler = ChildCompletion(this, state, child, proposedUpdate).asHandler
925961
)
926962
if (handle !== NonDisposableHandle) return true // child is not complete and we've started waiting for it
927963
val nextChild = child.nextChild() ?: return false
928-
return tryWaitForChild(state, nextChild, proposedUpdate)
964+
return tryWaitForChildren(state, nextChild, proposedUpdate)
929965
}
930966

931967
// ## IMPORTANT INVARIANT: Only one thread can be concurrently invoking this method.
@@ -934,7 +970,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
934970
// figure out if we need to wait for next child
935971
val waitChild = lastChild.nextChild()
936972
// try wait for next child
937-
if (waitChild != null && tryWaitForChild(state, waitChild, proposedUpdate)) return // waiting for next child
973+
if (waitChild != null && tryWaitForChildren(state, waitChild, proposedUpdate)) return // waiting for next child
938974
// no more children to wait -- try update state
939975
val finalState = finalizeFinishingState(state, proposedUpdate)
940976
afterCompletion(finalState)

kotlinx-coroutines-core/jvm/test/MemoryFootprintTest.kt

+25-2
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,32 @@
55
package kotlinx.coroutines
66

77
import org.junit.Test
8-
import org.openjdk.jol.info.ClassLayout
8+
import org.openjdk.jol.info.*
99
import kotlin.test.*
1010

1111

1212
class MemoryFootprintTest : TestBase(true) {
1313

1414
@Test
15-
fun testJobLayout() = assertLayout(Job().javaClass, 24)
15+
fun testJobLayout() {
16+
assertLayout(jobWithChildren(0).javaClass, 24)
17+
}
18+
19+
@Test
20+
fun testJobSize() {
21+
assertTotalSize(jobWithChildren(1), 112)
22+
assertTotalSize(jobWithChildren(2), 192) // + 80
23+
assertTotalSize(jobWithChildren(3), 248) // + 56
24+
assertTotalSize(jobWithChildren(4), 304) // + 56
25+
}
26+
27+
private fun jobWithChildren(numberOfChildren: Int): Job {
28+
val result = Job()
29+
repeat(numberOfChildren) {
30+
Job(result)
31+
}
32+
return result
33+
}
1634

1735
@Test
1836
fun testCancellableContinuationFootprint() = assertLayout(CancellableContinuationImpl::class.java, 48)
@@ -22,4 +40,9 @@ class MemoryFootprintTest : TestBase(true) {
2240
// println(ClassLayout.parseClass(clz).toPrintable())
2341
assertEquals(expectedSize.toLong(), size)
2442
}
43+
44+
private fun assertTotalSize(instance: Job, expectedSize: Int) {
45+
val size = GraphLayout.parseInstance(instance).totalSize()
46+
assertEquals(expectedSize.toLong(), size)
47+
}
2548
}

0 commit comments

Comments
 (0)