Skip to content

Commit 2b1e276

Browse files
SokolovaMariamvicsokolovaqwwdfsad
authored
JobSupport: non-atomic adding to a Job's list of listeners (#2096)
* Get rid of DCSS (double compare single swap) primitive in JobSupport in order to reduce both b binary size and performance of list addition Co-authored-by: SokolovaMaria <[email protected]> Co-authored-by: Vsevolod Tolstopyatov <[email protected]>
1 parent e2a6827 commit 2b1e276

File tree

10 files changed

+136
-110
lines changed

10 files changed

+136
-110
lines changed

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,8 @@ private class AwaitAll<T>(private val deferreds: Array<out Deferred<T>>) {
107107
var disposer: DisposeHandlersOnCancel?
108108
get() = _disposer.value
109109
set(value) { _disposer.value = value }
110-
111-
override fun invoke(cause: Throwable?) {
110+
111+
override fun invokeOnce(cause: Throwable?) {
112112
if (cause != null) {
113113
val token = continuation.tryResumeWithException(cause)
114114
if (token != null) {

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

+2
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,6 @@ internal expect val CancelHandlerBase.asHandler: CompletionHandler
4444
// because we play type tricks on Kotlin/JS and handler is not necessarily a function there
4545
internal expect fun CompletionHandler.invokeIt(cause: Throwable?)
4646

47+
// :KLUDGE: We have to use `isHandlerOf` extension, because performing this type check directly in the code
48+
// causes Incompatible types error during Kotlin/JS compilation
4749
internal inline fun <reified T> CompletionHandler.isHandlerOf(): Boolean = this is T

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

+117-46
Original file line numberDiff line numberDiff line change
@@ -456,51 +456,86 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
456456
// Create node upfront -- for common cases it just initializes JobNode.job field,
457457
// for user-defined handlers it allocates a JobNode object that we might not need, but this is Ok.
458458
val node: JobNode = makeNode(handler, onCancelling)
459+
/*
460+
* LinkedList algorithm does not support removing and re-adding the same node,
461+
* so here we check whether the node is already added to the list to avoid adding the node twice.
462+
*/
463+
var isNodeAdded = false
459464
loopOnState { state ->
460465
when (state) {
461466
is Empty -> { // EMPTY_X state -- no completion handlers
462467
if (state.isActive) {
463468
// try move to SINGLE state
464469
if (_state.compareAndSet(state, node)) return node
465-
} else
470+
} else {
466471
promoteEmptyToNodeList(state) // that way we can add listener for non-active coroutine
472+
}
467473
}
468474
is Incomplete -> {
469475
val list = state.list
470476
if (list == null) { // SINGLE/SINGLE+
471477
promoteSingleToNodeList(state as JobNode)
472-
} else {
473-
var rootCause: Throwable? = null
474-
var handle: DisposableHandle = NonDisposableHandle
475-
if (onCancelling && state is Finishing) {
476-
synchronized(state) {
477-
// check if we are installing cancellation handler on job that is being cancelled
478-
rootCause = state.rootCause // != null if cancelling job
479-
// We add node to the list in two cases --- either the job is not being cancelled
480-
// or we are adding a child to a coroutine that is not completing yet
481-
if (rootCause == null || handler.isHandlerOf<ChildHandleNode>() && !state.isCompleting) {
482-
// Note: add node the list while holding lock on state (make sure it cannot change)
483-
if (!addLastAtomic(state, list, node)) return@loopOnState // retry
484-
// just return node if we don't have to invoke handler (not cancelling yet)
485-
if (rootCause == null) return node
486-
// otherwise handler is invoked immediately out of the synchronized section & handle returned
487-
handle = node
478+
return@loopOnState // retry
479+
}
480+
// ...else {, but without nesting
481+
var rootCause: Throwable? = null
482+
var handle: DisposableHandle = NonDisposableHandle
483+
if (onCancelling && state is Finishing) {
484+
synchronized(state) {
485+
// check if we are installing cancellation handler on job that is being cancelled
486+
rootCause = state.rootCause // != null if cancelling job
487+
// We add node to the list in two cases --- either the job is not being cancelled
488+
// or we are adding a child to a coroutine that is not completing yet
489+
if (rootCause == null || handler.isHandlerOf<ChildHandleNode>() && !state.isCompleting) {
490+
// Note: add node the list while holding lock on state (make sure it cannot change)
491+
if (!isNodeAdded) list.addLast(node)
492+
if (this.state !== state) {
493+
/*
494+
* Here we have an additional check for ChildCompletion. Invoking the handler once is not enough
495+
* for this particular kind of node -- the caller makes a decision based on whether the node was added
496+
* or not and that decision should be made **once**.
497+
* To be more precise, the caller of ChildCompletion, in case when it's the last child,
498+
* should make a decision whether to start transition to the final state, based on
499+
* whether the ChildCompletion was added to the list or not. If not -- the JobNode.invoke will do that.
500+
* See comment to JobNode.invoke, we cannot differentiate the situation when external state updater
501+
* invoked or skipped the node, thus we additionally synchronize on 'markInvoked'.
502+
*/
503+
if (node is ChildCompletion && !node.markInvoked()) return node
504+
// The state can only change to Complete here, so the node can stay in the list, just retry
505+
return@loopOnState
488506
}
507+
// just return node if we don't have to invoke handler (not cancelling yet)
508+
if (rootCause == null) return node
509+
// otherwise handler is invoked immediately out of the synchronized section & handle returned
510+
handle = node
489511
}
490512
}
491-
if (rootCause != null) {
492-
// Note: attachChild uses invokeImmediately, so it gets invoked when adding to cancelled job
493-
if (invokeImmediately) handler.invokeIt(rootCause)
494-
return handle
513+
}
514+
if (rootCause != null) {
515+
// Note: attachChild uses invokeImmediately, so it gets invoked when adding to cancelled job
516+
if (invokeImmediately) {
517+
node.invoke(rootCause)
518+
}
519+
return handle
520+
} else {
521+
if (!isNodeAdded) list.addLast(node)
522+
if (this.state !== state) {
523+
// Here again, we try to prevent concurrent finalization of the parent,
524+
// if the parent fails to add ChildCompletion because the child changed it's state to Completed.
525+
if (node is ChildCompletion && !node.markInvoked()) return node
526+
// If the state has changed to Complete, the node can stay in the list, just retry.
527+
if (this.state !is Incomplete) return@loopOnState
528+
// If the state is Incomplete, set the flag that the node is already added to the list instead of removing it.
529+
isNodeAdded = true
495530
} else {
496-
if (addLastAtomic(state, list, node)) return node
531+
return node
497532
}
498533
}
499534
}
500535
else -> { // is complete
501-
// :KLUDGE: We have to invoke a handler in platform-specific way via `invokeIt` extension,
502-
// because we play type tricks on Kotlin/JS and handler is not necessarily a function there
503-
if (invokeImmediately) handler.invokeIt((state as? CompletedExceptionally)?.cause)
536+
if (invokeImmediately) {
537+
node.invoke((state as? CompletedExceptionally)?.cause)
538+
}
504539
return NonDisposableHandle
505540
}
506541
}
@@ -520,9 +555,6 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
520555
return node
521556
}
522557

523-
private fun addLastAtomic(expect: Any, list: NodeList, node: JobNode) =
524-
list.addLastIf(node) { this.state === expect }
525-
526558
private fun promoteEmptyToNodeList(state: Empty) {
527559
// try to promote it to LIST state with the corresponding state
528560
val list = NodeList()
@@ -596,7 +628,13 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
596628
}
597629
is Incomplete -> { // may have a list of completion handlers
598630
// remove node from the list if there is a list
599-
if (state.list != null) node.remove()
631+
if (state.list != null) {
632+
if (!node.remove() && node.isRemoved) {
633+
// Note: .remove() returns 'false' if the node wasn't added at all, e.g.
634+
// because it was an optimized "single element list"
635+
node.helpRemove()
636+
}
637+
}
600638
return
601639
}
602640
else -> return // it is complete and does not have any completion handlers
@@ -814,7 +852,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
814852
}
815853
}
816854
}
817-
}
855+
}
818856

819857
/**
820858
* Completes this job. Used by [AbstractCoroutine.resume].
@@ -1151,9 +1189,11 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
11511189
private val child: ChildHandleNode,
11521190
private val proposedUpdate: Any?
11531191
) : JobNode() {
1154-
override fun invoke(cause: Throwable?) {
1192+
override fun invokeOnce(cause: Throwable?) {
11551193
parent.continueCompleting(state, child, proposedUpdate)
11561194
}
1195+
override fun toString(): String =
1196+
"ChildCompletion[$child, $proposedUpdate]"
11571197
}
11581198

11591199
private class AwaitContinuation<T>(
@@ -1355,6 +1395,31 @@ internal abstract class JobNode : CompletionHandlerBase(), DisposableHandle, Inc
13551395
override val isActive: Boolean get() = true
13561396
override val list: NodeList? get() = null
13571397
override fun dispose() = job.removeNode(this)
1398+
private val isInvoked = atomic(false)
1399+
1400+
/*
1401+
* This method is guaranteed to be invoked once per JobNode lifecycle.
1402+
*/
1403+
protected abstract fun invokeOnce(cause: Throwable?)
1404+
1405+
/*
1406+
* This method can be invoked more than once thus it's protected
1407+
* with atomic flag.
1408+
*
1409+
* It can be invoked twice via [invokeOnCompletion(invokeImmediately = true)]
1410+
* when addLastIf fails due to the race with Job state update.
1411+
* In that case, we cannot distinguish the situation when the node was added
1412+
* and the external state updater actually invoked it from the situation
1413+
* when the state updater already invoked all elements from a list and then
1414+
* we added a new node to already abandoned list.
1415+
* So, when addLastIf fails, we invoke handler in-place, using
1416+
* markInvoked as protection against the former case.
1417+
*/
1418+
final override fun invoke(cause: Throwable?) {
1419+
if (!markInvoked()) return
1420+
invokeOnce(cause)
1421+
}
1422+
fun markInvoked() = isInvoked.compareAndSet(false, true)
13581423
override fun toString() = "$classSimpleName@$hexAddress[job@${job.hexAddress}]"
13591424
}
13601425

@@ -1387,20 +1452,22 @@ internal class InactiveNodeList(
13871452

13881453
private class InvokeOnCompletion(
13891454
private val handler: CompletionHandler
1390-
) : JobNode() {
1391-
override fun invoke(cause: Throwable?) = handler.invoke(cause)
1455+
) : JobNode() {
1456+
override fun invokeOnce(cause: Throwable?) = handler.invoke(cause)
1457+
override fun toString() = "InvokeOnCompletion[$classSimpleName@$hexAddress]"
13921458
}
13931459

13941460
private class ResumeOnCompletion(
13951461
private val continuation: Continuation<Unit>
13961462
) : JobNode() {
1397-
override fun invoke(cause: Throwable?) = continuation.resume(Unit)
1463+
override fun invokeOnce(cause: Throwable?) = continuation.resume(Unit)
1464+
override fun toString() = "ResumeOnCompletion[$continuation]"
13981465
}
13991466

14001467
private class ResumeAwaitOnCompletion<T>(
14011468
private val continuation: CancellableContinuationImpl<T>
14021469
) : JobNode() {
1403-
override fun invoke(cause: Throwable?) {
1470+
override fun invokeOnce(cause: Throwable?) {
14041471
val state = job.state
14051472
assert { state !is Incomplete }
14061473
if (state is CompletedExceptionally) {
@@ -1412,32 +1479,36 @@ private class ResumeAwaitOnCompletion<T>(
14121479
continuation.resume(state.unboxState() as T)
14131480
}
14141481
}
1482+
override fun toString() = "ResumeAwaitOnCompletion[$continuation]"
14151483
}
14161484

14171485
internal class DisposeOnCompletion(
14181486
private val handle: DisposableHandle
14191487
) : JobNode() {
1420-
override fun invoke(cause: Throwable?) = handle.dispose()
1488+
override fun invokeOnce(cause: Throwable?) = handle.dispose()
1489+
override fun toString(): String = "DisposeOnCompletion[${handle}]"
14211490
}
14221491

14231492
private class SelectJoinOnCompletion<R>(
14241493
private val select: SelectInstance<R>,
14251494
private val block: suspend () -> R
14261495
) : JobNode() {
1427-
override fun invoke(cause: Throwable?) {
1496+
override fun invokeOnce(cause: Throwable?) {
14281497
if (select.trySelect())
14291498
block.startCoroutineCancellable(select.completion)
14301499
}
1500+
override fun toString(): String = "SelectJoinOnCompletion[$select]"
14311501
}
14321502

14331503
private class SelectAwaitOnCompletion<T, R>(
14341504
private val select: SelectInstance<R>,
14351505
private val block: suspend (T) -> R
14361506
) : JobNode() {
1437-
override fun invoke(cause: Throwable?) {
1507+
override fun invokeOnce(cause: Throwable?) {
14381508
if (select.trySelect())
14391509
job.selectAwaitCompletion(select, block)
14401510
}
1511+
override fun toString(): String = "SelectAwaitOnCompletion[$select]"
14411512
}
14421513

14431514
// -------- invokeOnCancellation nodes
@@ -1450,28 +1521,28 @@ internal abstract class JobCancellingNode : JobNode()
14501521

14511522
private class InvokeOnCancelling(
14521523
private val handler: CompletionHandler
1453-
) : JobCancellingNode() {
1524+
) : JobCancellingNode() {
14541525
// delegate handler shall be invoked at most once, so here is an additional flag
1455-
private val _invoked = atomic(0) // todo: replace with atomic boolean after migration to recent atomicFu
1456-
override fun invoke(cause: Throwable?) {
1457-
if (_invoked.compareAndSet(0, 1)) handler.invoke(cause)
1458-
}
1526+
override fun invokeOnce(cause: Throwable?) = handler.invoke(cause)
1527+
override fun toString() = "InvokeOnCancelling[$classSimpleName@$hexAddress]"
14591528
}
14601529

14611530
internal class ChildHandleNode(
14621531
@JvmField val childJob: ChildJob
14631532
) : JobCancellingNode(), ChildHandle {
14641533
override val parent: Job get() = job
1465-
override fun invoke(cause: Throwable?) = childJob.parentCancelled(job)
1534+
override fun invokeOnce(cause: Throwable?) = childJob.parentCancelled(job)
14661535
override fun childCancelled(cause: Throwable): Boolean = job.childCancelled(cause)
1536+
override fun toString(): String = "ChildHandle[$childJob]"
14671537
}
14681538

14691539
// Same as ChildHandleNode, but for cancellable continuation
14701540
internal class ChildContinuation(
14711541
@JvmField val child: CancellableContinuationImpl<*>
14721542
) : JobCancellingNode() {
1473-
override fun invoke(cause: Throwable?) {
1543+
override fun invokeOnce(cause: Throwable?) =
14741544
child.parentCancelled(child.getContinuationCancellationCause(job))
1475-
}
1545+
override fun toString(): String =
1546+
"ChildContinuation[$child]"
14761547
}
14771548

kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt

-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ public expect open class LockFreeLinkedListNode() {
1515
public val prevNode: LockFreeLinkedListNode
1616
public fun addLast(node: LockFreeLinkedListNode)
1717
public fun addOneIfEmpty(node: LockFreeLinkedListNode): Boolean
18-
public inline fun addLastIf(node: LockFreeLinkedListNode, crossinline condition: () -> Boolean): Boolean
1918
public inline fun addLastIfPrev(
2019
node: LockFreeLinkedListNode,
2120
predicate: (LockFreeLinkedListNode) -> Boolean

kotlinx-coroutines-core/common/src/selects/Select.kt

+3-2
Original file line numberDiff line numberDiff line change
@@ -336,9 +336,10 @@ internal class SelectBuilderImpl<in R>(
336336

337337
private inner class SelectOnCancelling : JobCancellingNode() {
338338
// Note: may be invoked multiple times, but only the first trySelect succeeds anyway
339-
override fun invoke(cause: Throwable?) {
340-
if (trySelect())
339+
override fun invokeOnce(cause: Throwable?) {
340+
if (trySelect()) {
341341
resumeSelectWithException(job.getCancellationException())
342+
}
342343
}
343344
}
344345

kotlinx-coroutines-core/concurrent/src/internal/LockFreeLinkedList.kt

+3-21
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,6 @@ public actual open class LockFreeLinkedListNode {
8686
}
8787
}
8888

89-
@PublishedApi
90-
internal inline fun makeCondAddOp(node: Node, crossinline condition: () -> Boolean): CondAddOp =
91-
object : CondAddOp(node) {
92-
override fun prepare(affected: Node): Any? = if (condition()) null else CONDITION_FALSE
93-
}
94-
9589
public actual open val isRemoved: Boolean get() = next is Removed
9690

9791
// LINEARIZABLE. Returns Node | Removed
@@ -147,20 +141,6 @@ public actual open class LockFreeLinkedListNode {
147141

148142
public fun <T : Node> describeAddLast(node: T): AddLastDesc<T> = AddLastDesc(this, node)
149143

150-
/**
151-
* Adds last item to this list atomically if the [condition] is true.
152-
*/
153-
public actual inline fun addLastIf(node: Node, crossinline condition: () -> Boolean): Boolean {
154-
val condAdd = makeCondAddOp(node, condition)
155-
while (true) { // lock-free loop on prev.next
156-
val prev = prevNode // sentinel node is never removed, so prev is always defined
157-
when (prev.tryCondAddNext(node, this, condAdd)) {
158-
SUCCESS -> return true
159-
FAILURE -> return false
160-
}
161-
}
162-
}
163-
164144
public actual inline fun addLastIfPrev(node: Node, predicate: (Node) -> Boolean): Boolean {
165145
while (true) { // lock-free loop on prev.next
166146
val prev = prevNode // sentinel node is never removed, so prev is always defined
@@ -174,7 +154,9 @@ public actual open class LockFreeLinkedListNode {
174154
predicate: (Node) -> Boolean, // prev node predicate
175155
crossinline condition: () -> Boolean // atomically checked condition
176156
): Boolean {
177-
val condAdd = makeCondAddOp(node, condition)
157+
val condAdd = object : CondAddOp(node) {
158+
override fun prepare(affected: Node): Any? = if (condition()) null else CONDITION_FALSE
159+
}
178160
while (true) { // lock-free loop on prev.next
179161
val prev = prevNode // sentinel node is never removed, so prev is always defined
180162
if (!predicate(prev)) return false

kotlinx-coroutines-core/concurrent/test/internal/LockFreeLinkedListTest.kt

-14
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,6 @@ class LockFreeLinkedListTest {
3232
assertContents(list)
3333
}
3434

35-
@Test
36-
fun testCondOps() {
37-
val list = LockFreeLinkedListHead()
38-
assertContents(list)
39-
assertTrue(list.addLastIf(IntNode(1)) { true })
40-
assertContents(list, 1)
41-
assertFalse(list.addLastIf(IntNode(2)) { false })
42-
assertContents(list, 1)
43-
assertTrue(list.addLastIf(IntNode(3)) { true })
44-
assertContents(list, 1, 3)
45-
assertFalse(list.addLastIf(IntNode(4)) { false })
46-
assertContents(list, 1, 3)
47-
}
48-
4935
@Test
5036
fun testAtomicOpsSingle() {
5137
val list = LockFreeLinkedListHead()

0 commit comments

Comments
 (0)