Skip to content

Commit 2803a33

Browse files
authored
Remove DCSS (#4053)
The internal implementation of `JobSupport` no longer uses the Double-Compare Single-Swap algorithm. Instead, the signal for the list to stop accepting this or that kind of elements is provided explicitly. In addition to simplifying the implementation somewhat, this change allowed us to more precisely define when child nodes should stop being accepted into the list, fixing a bug. Fixes #3893 Additionally, new stress tests are added to ensure the correct behavior.
1 parent b34dc86 commit 2803a33

File tree

11 files changed

+496
-233
lines changed

11 files changed

+496
-233
lines changed

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

+184-66
Large diffs are not rendered by default.

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

-67
This file was deleted.

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

+6-6
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,22 @@
22

33
package kotlinx.coroutines.internal
44

5-
import kotlinx.coroutines.*
6-
import kotlin.jvm.*
7-
85
/** @suppress **This is unstable API and it is subject to change.** */
96
public expect open class LockFreeLinkedListNode() {
107
public val isRemoved: Boolean
118
public val nextNode: LockFreeLinkedListNode
129
public val prevNode: LockFreeLinkedListNode
10+
public fun addLast(node: LockFreeLinkedListNode, permissionsBitmask: Int): Boolean
1311
public fun addOneIfEmpty(node: LockFreeLinkedListNode): Boolean
14-
public inline fun addLastIf(node: LockFreeLinkedListNode, crossinline condition: () -> Boolean): Boolean
1512
public open fun remove(): Boolean
1613

14+
/**
15+
* Closes the list for anything that requests the permission [forbiddenElementsBit].
16+
* Only a single permission can be forbidden at a time, but this isn't checked.
17+
*/
18+
public fun close(forbiddenElementsBit: Int)
1719
}
1820

19-
internal fun LockFreeLinkedListNode.addLast(node: LockFreeLinkedListNode) = addLastIf(node) { true }
20-
2121
/** @suppress **This is unstable API and it is subject to change.** */
2222
public expect open class LockFreeLinkedListHead() : LockFreeLinkedListNode {
2323
public inline fun forEach(block: (LockFreeLinkedListNode) -> Unit)

kotlinx-coroutines-core/common/test/JobTest.kt

+14
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,20 @@ class JobTest : TestBase() {
174174
finish(4)
175175
}
176176

177+
@Test
178+
fun testInvokeOnCancellingFiringOnNormalExit() = runTest {
179+
val job = launch {
180+
expect(2)
181+
}
182+
job.invokeOnCompletion(onCancelling = true) {
183+
assertNull(it)
184+
expect(3)
185+
}
186+
expect(1)
187+
job.join()
188+
finish(4)
189+
}
190+
177191
@Test
178192
fun testOverriddenParent() = runTest {
179193
val parent = Job()

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

+18-63
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,6 @@ import kotlin.jvm.*
88

99
private typealias Node = LockFreeLinkedListNode
1010

11-
@PublishedApi
12-
internal const val UNDECIDED: Int = 0
13-
14-
@PublishedApi
15-
internal const val SUCCESS: Int = 1
16-
17-
@PublishedApi
18-
internal const val FAILURE: Int = 2
19-
20-
@PublishedApi
21-
internal val CONDITION_FALSE: Any = Symbol("CONDITION_FALSE")
22-
2311
/**
2412
* Doubly-linked concurrent list node with remove support.
2513
* Based on paper
@@ -49,37 +37,10 @@ public actual open class LockFreeLinkedListNode {
4937
private fun removed(): Removed =
5038
_removedRef.value ?: Removed(this).also { _removedRef.lazySet(it) }
5139

52-
@PublishedApi
53-
internal abstract class CondAddOp(
54-
@JvmField val newNode: Node
55-
) : AtomicOp<Node>() {
56-
@JvmField var oldNext: Node? = null
57-
58-
override fun complete(affected: Node, failure: Any?) {
59-
val success = failure == null
60-
val update = if (success) newNode else oldNext
61-
if (update != null && affected._next.compareAndSet( this, update)) {
62-
// only the thread the makes this update actually finishes add operation
63-
if (success) newNode.finishAdd(oldNext!!)
64-
}
65-
}
66-
}
67-
68-
@PublishedApi
69-
internal inline fun makeCondAddOp(node: Node, crossinline condition: () -> Boolean): CondAddOp =
70-
object : CondAddOp(node) {
71-
override fun prepare(affected: Node): Any? = if (condition()) null else CONDITION_FALSE
72-
}
73-
7440
public actual open val isRemoved: Boolean get() = next is Removed
7541

7642
// LINEARIZABLE. Returns Node | Removed
77-
public val next: Any get() {
78-
_next.loop { next ->
79-
if (next !is OpDescriptor) return next
80-
next.perform(this)
81-
}
82-
}
43+
public val next: Any get() = _next.value
8344

8445
// LINEARIZABLE. Returns next non-removed Node
8546
public actual val nextNode: Node get() =
@@ -117,20 +78,27 @@ public actual open class LockFreeLinkedListNode {
11778
// ------ addLastXXX ------
11879

11980
/**
120-
* Adds last item to this list atomically if the [condition] is true.
81+
* Adds last item to this list. Returns `false` if the list is closed.
12182
*/
122-
public actual inline fun addLastIf(node: Node, crossinline condition: () -> Boolean): Boolean {
123-
val condAdd = makeCondAddOp(node, condition)
83+
public actual fun addLast(node: Node, permissionsBitmask: Int): Boolean {
12484
while (true) { // lock-free loop on prev.next
125-
val prev = prevNode // sentinel node is never removed, so prev is always defined
126-
when (prev.tryCondAddNext(node, this, condAdd)) {
127-
SUCCESS -> return true
128-
FAILURE -> return false
85+
val currentPrev = prevNode
86+
return when {
87+
currentPrev is ListClosed ->
88+
currentPrev.forbiddenElementsBitmask and permissionsBitmask == 0 &&
89+
currentPrev.addLast(node, permissionsBitmask)
90+
currentPrev.addNext(node, this) -> true
91+
else -> continue
12992
}
13093
}
13194
}
13295

133-
// ------ addXXX util ------
96+
/**
97+
* Forbids adding new items to this list.
98+
*/
99+
public actual fun close(forbiddenElementsBit: Int) {
100+
addLast(ListClosed(forbiddenElementsBit), forbiddenElementsBit)
101+
}
134102

135103
/**
136104
* Given:
@@ -165,17 +133,6 @@ public actual open class LockFreeLinkedListNode {
165133
return true
166134
}
167135

168-
// returns UNDECIDED, SUCCESS or FAILURE
169-
@PublishedApi
170-
internal fun tryCondAddNext(node: Node, next: Node, condAdd: CondAddOp): Int {
171-
node._prev.lazySet(this)
172-
node._next.lazySet(next)
173-
condAdd.oldNext = next
174-
if (!_next.compareAndSet(next, condAdd)) return UNDECIDED
175-
// added operation successfully (linearized) -- complete it & fixup the list
176-
return if (condAdd.perform(this) == null) SUCCESS else FAILURE
177-
}
178-
179136
// ------ removeXXX ------
180137

181138
/**
@@ -273,10 +230,6 @@ public actual open class LockFreeLinkedListNode {
273230
}
274231
// slow path when we need to help remove operations
275232
this.isRemoved -> return null // nothing to do, this node was removed, bail out asap to save time
276-
prevNext is OpDescriptor -> { // help & retry
277-
prevNext.perform(prev)
278-
return correctPrev() // retry from scratch
279-
}
280233
prevNext is Removed -> {
281234
if (last !== null) {
282235
// newly added (prev) node is already removed, correct last.next around it
@@ -332,3 +285,5 @@ public actual open class LockFreeLinkedListHead : LockFreeLinkedListNode() {
332285
// optimization: because head is never removed, we don't have to read _next.value to check these:
333286
override val isRemoved: Boolean get() = false
334287
}
288+
289+
private class ListClosed(@JvmField val forbiddenElementsBitmask: Int): LockFreeLinkedListNode()

kotlinx-coroutines-core/jsAndWasmShared/src/internal/LinkedList.kt

+17-18
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,20 @@ public actual open class LockFreeLinkedListNode {
1414
inline actual val prevNode get() = _prev
1515
inline actual val isRemoved get() = _removed
1616

17-
@PublishedApi
18-
internal fun addLast(node: Node) {
19-
val prev = this._prev
20-
node._next = this
21-
node._prev = prev
22-
prev._next = node
23-
this._prev = node
17+
public actual fun addLast(node: Node, permissionsBitmask: Int): Boolean = when (val prev = this._prev) {
18+
is ListClosed ->
19+
prev.forbiddenElementsBitmask and permissionsBitmask == 0 && prev.addLast(node, permissionsBitmask)
20+
else -> {
21+
node._next = this
22+
node._prev = prev
23+
prev._next = node
24+
this._prev = node
25+
true
26+
}
27+
}
28+
29+
public actual fun close(forbiddenElementsBit: Int) {
30+
addLast(ListClosed(forbiddenElementsBit), forbiddenElementsBit)
2431
}
2532

2633
/*
@@ -30,10 +37,6 @@ public actual open class LockFreeLinkedListNode {
3037
* invokes handler on remove
3138
*/
3239
public actual open fun remove(): Boolean {
33-
return removeImpl()
34-
}
35-
36-
private fun removeImpl(): Boolean {
3740
if (_removed) return false
3841
val prev = this._prev
3942
val next = this._next
@@ -45,13 +48,7 @@ public actual open class LockFreeLinkedListNode {
4548

4649
public actual fun addOneIfEmpty(node: Node): Boolean {
4750
if (_next !== this) return false
48-
addLast(node)
49-
return true
50-
}
51-
52-
public actual inline fun addLastIf(node: Node, crossinline condition: () -> Boolean): Boolean {
53-
if (!condition()) return false
54-
addLast(node)
51+
addLast(node, Int.MIN_VALUE)
5552
return true
5653
}
5754
}
@@ -72,3 +69,5 @@ public actual open class LockFreeLinkedListHead : Node() {
7269
// just a defensive programming -- makes sure that list head sentinel is never removed
7370
public actual final override fun remove(): Nothing = throw UnsupportedOperationException()
7471
}
72+
73+
private class ListClosed(val forbiddenElementsBitmask: Int): LockFreeLinkedListNode()

kotlinx-coroutines-core/jsAndWasmShared/test/internal/LinkedListTest.kt

+4-4
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ class LinkedListTest {
1212
fun testSimpleAddLastRemove() {
1313
val list = LockFreeLinkedListHead()
1414
assertContents(list)
15-
val n1 = IntNode(1).apply { list.addLast(this) }
15+
val n1 = IntNode(1).apply { list.addLast(this, Int.MAX_VALUE) }
1616
assertContents(list, 1)
17-
val n2 = IntNode(2).apply { list.addLast(this) }
17+
val n2 = IntNode(2).apply { list.addLast(this, Int.MAX_VALUE) }
1818
assertContents(list, 1, 2)
19-
val n3 = IntNode(3).apply { list.addLast(this) }
19+
val n3 = IntNode(3).apply { list.addLast(this, Int.MAX_VALUE) }
2020
assertContents(list, 1, 2, 3)
21-
val n4 = IntNode(4).apply { list.addLast(this) }
21+
val n4 = IntNode(4).apply { list.addLast(this, Int.MAX_VALUE) }
2222
assertContents(list, 1, 2, 3, 4)
2323
assertTrue(n1.remove())
2424
assertContents(list, 2, 3, 4)

0 commit comments

Comments
 (0)