Skip to content

Commit 33fdfa8

Browse files
committed
Minimize the API surface of LockFreeLinkedList
With this change, `JobSupport` uses only a small and well-defined portion of the functionality `LockFreeLinkedList` provides, which makes it easier to replace the list implementation.
1 parent 4858fc5 commit 33fdfa8

File tree

5 files changed

+92
-78
lines changed

5 files changed

+92
-78
lines changed

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

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -559,11 +559,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
559559

560560
private fun promoteSingleToNodeList(state: JobNode) {
561561
// try to promote it to list (SINGLE+ state)
562-
state.addOneIfEmpty(NodeList())
563-
// it must be in SINGLE+ state or state has changed (node could have need removed from state)
564-
val list = state.nextNode // either our NodeList or somebody else won the race, updated state
565-
// just attempt converting it to list if state is still the same, then we'll continue lock-free loop
566-
_state.compareAndSet(state, list)
562+
_state.compareAndSet(state, state.attachToList(NodeList()))
567563
}
568564

569565
public final override suspend fun join() {
@@ -951,37 +947,33 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
951947
handler = ChildCompletion(this, state, child, proposedUpdate)
952948
)
953949
if (handle !== NonDisposableHandle) return true // child is not complete and we've started waiting for it
954-
val nextChild = child.nextChild() ?: return false
950+
val nextChild = state.list.nextChild(startAfter = child) ?: return false
955951
return tryWaitForChild(state, nextChild, proposedUpdate)
956952
}
957953

958954
// ## IMPORTANT INVARIANT: Only one thread can be concurrently invoking this method.
959955
private fun continueCompleting(state: Finishing, lastChild: ChildHandleNode, proposedUpdate: Any?) {
960956
assert { this.state === state } // consistency check -- it cannot change while we are waiting for children
961957
// figure out if we need to wait for next child
962-
val waitChild = lastChild.nextChild()
958+
val waitChild = state.list.nextChild(startAfter = lastChild)
963959
// try wait for next child
964960
if (waitChild != null && tryWaitForChild(state, waitChild, proposedUpdate)) return // waiting for next child
965961
// no more children to wait -- stop accepting children
966962
state.list.close(LIST_CHILD_PERMISSION)
967963
// did any children get added?
968-
val waitChildAgain = lastChild.nextChild()
964+
val waitChildAgain = state.list.nextChild(startAfter = lastChild)
969965
// try wait for next child
970966
if (waitChildAgain != null && tryWaitForChild(state, waitChildAgain, proposedUpdate)) return // waiting for next child
971967
// no more children, now we are sure; try to update the state
972968
val finalState = finalizeFinishingState(state, proposedUpdate)
973969
afterCompletion(finalState)
974970
}
975971

976-
private fun LockFreeLinkedListNode.nextChild(): ChildHandleNode? {
977-
var cur = this
978-
while (cur.isRemoved) cur = cur.prevNode // rollback to prev non-removed (or list head)
979-
while (true) {
980-
cur = cur.nextNode
981-
if (cur.isRemoved) continue
982-
if (cur is ChildHandleNode) return cur
983-
if (cur is NodeList) return null // checked all -- no more children
972+
private fun NodeList.nextChild(startAfter: LockFreeLinkedListNode? = null): ChildHandleNode? {
973+
forEach(startAfter) {
974+
if (it is ChildHandleNode) return it
984975
}
976+
return null
985977
}
986978

987979
public final override val children: Sequence<Job> get() = sequence {
@@ -1046,7 +1038,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
10461038
* as this child didn't make it before [notifyCancelling] and won't be notified that it should be
10471039
* cancelled.
10481040
*
1049-
* And if the parent wasn't cancelled and the previous [LockFreeLinkedListNode.addLast] failed because
1041+
* And if the parent wasn't cancelled and the previous [LockFreeLinkedListHead.addLast] failed because
10501042
* the job is in its final state already, we won't be able to attach anyway, so we must just invoke
10511043
* the handler and return.
10521044
*/

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

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,38 @@ package kotlinx.coroutines.internal
44

55
/** @suppress **This is unstable API and it is subject to change.** */
66
public expect open class LockFreeLinkedListNode() {
7-
public val isRemoved: Boolean
8-
public val nextNode: LockFreeLinkedListNode
9-
public val prevNode: LockFreeLinkedListNode
10-
public fun addLast(node: LockFreeLinkedListNode, permissionsBitmask: Int): Boolean
11-
public fun addOneIfEmpty(node: LockFreeLinkedListNode): Boolean
12-
public open fun remove(): Boolean
7+
/**
8+
* Try putting `node` into a list.
9+
*
10+
* Returns:
11+
* - The new head of the list if the operation succeeded.
12+
* - The head of the list if someone else concurrently added this node to the list,
13+
* but no other modifications to the list were made.
14+
* - Some garbage if the list was already edited.
15+
*/
16+
public fun attachToList(node: LockFreeLinkedListHead): LockFreeLinkedListNode
17+
18+
/**
19+
* Remove this node from the list.
20+
*/
21+
public open fun remove()
22+
}
23+
24+
/** @suppress **This is unstable API and it is subject to change.** */
25+
public expect open class LockFreeLinkedListHead() {
26+
/**
27+
* Iterates over all non-removed elements in this list, skipping every node until (and including) [startAfter].
28+
*/
29+
public inline fun forEach(startAfter: LockFreeLinkedListNode? = null, block: (LockFreeLinkedListNode) -> Unit)
1330

1431
/**
1532
* Closes the list for anything that requests the permission [forbiddenElementsBit].
1633
* Only a single permission can be forbidden at a time, but this isn't checked.
1734
*/
1835
public fun close(forbiddenElementsBit: Int)
19-
}
2036

21-
/** @suppress **This is unstable API and it is subject to change.** */
22-
public expect open class LockFreeLinkedListHead() : LockFreeLinkedListNode {
23-
public inline fun forEach(block: (LockFreeLinkedListNode) -> Unit)
24-
public final override fun remove(): Nothing
37+
/**
38+
* Adds the [node] to the end of the list if every bit in [permissionsBitmask] is still allowed in the list.
39+
*/
40+
public fun addLast(node: LockFreeLinkedListNode, permissionsBitmask: Int): Boolean
2541
}

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

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -37,21 +37,21 @@ public actual open class LockFreeLinkedListNode {
3737
private fun removed(): Removed =
3838
_removedRef.value ?: Removed(this).also { _removedRef.lazySet(it) }
3939

40-
public actual open val isRemoved: Boolean get() = next is Removed
40+
public open val isRemoved: Boolean get() = next is Removed
4141

4242
// LINEARIZABLE. Returns Node | Removed
4343
public val next: Any get() = _next.value
4444

4545
// LINEARIZABLE. Returns next non-removed Node
46-
public actual val nextNode: Node get() =
46+
val nextNode: Node get() =
4747
next.let { (it as? Removed)?.ref ?: it as Node } // unwraps the `next` node
4848

4949
// LINEARIZABLE WHEN THIS NODE IS NOT REMOVED:
5050
// Returns prev non-removed Node, makes sure prev is correct (prev.next === this)
5151
// NOTE: if this node is removed, then returns non-removed previous node without applying
5252
// prev.next correction, which does not provide linearizable backwards iteration, but can be used to
5353
// resume forward iteration when current node was removed.
54-
public actual val prevNode: Node
54+
public val prevNode: Node
5555
get() = correctPrev() ?: findPrevNonRemoved(_prev.value)
5656

5757
private tailrec fun findPrevNonRemoved(current: Node): Node {
@@ -61,16 +61,16 @@ public actual open class LockFreeLinkedListNode {
6161

6262
// ------ addOneIfEmpty ------
6363

64-
public actual fun addOneIfEmpty(node: Node): Boolean {
65-
node._prev.lazySet(this)
66-
node._next.lazySet(this)
64+
public actual fun attachToList(node: LockFreeLinkedListHead): Node {
65+
(node as Node)._prev.lazySet(this)
66+
(node as Node)._next.lazySet(this)
6767
while (true) {
6868
val next = next
69-
if (next !== this) return false // this is not an empty list!
69+
if (next !== this) return nextNode // this is not an empty list!
7070
if (_next.compareAndSet(this, node)) {
7171
// added successfully (linearized add) -- fixup the list
72-
node.finishAdd(this)
73-
return true
72+
(node as Node).finishAdd(this)
73+
return node
7474
}
7575
}
7676
}
@@ -80,7 +80,7 @@ public actual open class LockFreeLinkedListNode {
8080
/**
8181
* Adds last item to this list. Returns `false` if the list is closed.
8282
*/
83-
public actual fun addLast(node: Node, permissionsBitmask: Int): Boolean {
83+
fun addLast(node: Node, permissionsBitmask: Int): Boolean {
8484
while (true) { // lock-free loop on prev.next
8585
val currentPrev = prevNode
8686
return when {
@@ -93,13 +93,6 @@ public actual open class LockFreeLinkedListNode {
9393
}
9494
}
9595

96-
/**
97-
* Forbids adding new items to this list.
98-
*/
99-
public actual fun close(forbiddenElementsBit: Int) {
100-
addLast(ListClosed(forbiddenElementsBit), forbiddenElementsBit)
101-
}
102-
10396
/**
10497
* Given:
10598
* ```
@@ -142,8 +135,9 @@ public actual open class LockFreeLinkedListNode {
142135
* **Note**: Invocation of this operation does not guarantee that remove was actually complete if result was `false`.
143136
* In particular, invoking [nextNode].[prevNode] might still return this node even though it is "already removed".
144137
*/
145-
public actual open fun remove(): Boolean =
146-
removeOrNext() == null
138+
public actual open fun remove() {
139+
removeOrNext()
140+
}
147141

148142
// returns null if removed successfully or next node if this node is already removed
149143
@PublishedApi
@@ -271,19 +265,30 @@ public actual open class LockFreeLinkedListHead : LockFreeLinkedListNode() {
271265
/**
272266
* Iterates over all elements in this list of a specified type.
273267
*/
274-
public actual inline fun forEach(block: (Node) -> Unit) {
275-
var cur: Node = next as Node
276-
while (cur != this) {
277-
block(cur)
268+
public actual inline fun forEach(startAfter: LockFreeLinkedListNode?, block: (Node) -> Unit) {
269+
var cur: Node = startAfter ?: this
270+
while (cur.isRemoved) cur = cur.prevNode // rollback to prev non-removed (or list head)
271+
cur = cur.nextNode
272+
while (cur !== this) {
273+
if (!cur.isRemoved) {
274+
block(cur)
275+
}
278276
cur = cur.nextNode
279277
}
280278
}
281279

282280
// just a defensive programming -- makes sure that list head sentinel is never removed
283-
public actual final override fun remove(): Nothing = error("head cannot be removed")
281+
public final override fun remove(): Nothing = error("head cannot be removed")
284282

285283
// optimization: because head is never removed, we don't have to read _next.value to check these:
286284
override val isRemoved: Boolean get() = false
285+
286+
/**
287+
* Forbids adding new items to this list.
288+
*/
289+
public actual fun close(forbiddenElementsBit: Int) {
290+
addLast(ListClosed(forbiddenElementsBit), forbiddenElementsBit)
291+
}
287292
}
288293

289294
private class ListClosed(val forbiddenElementsBitmask: Int): LockFreeLinkedListNode()

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

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
package kotlinx.coroutines.internal
44

5+
import kotlinx.coroutines.*
6+
57
private typealias Node = LockFreeLinkedListNode
68

79
/** @suppress **This is unstable API and it is subject to change.** */
@@ -10,11 +12,7 @@ public actual open class LockFreeLinkedListNode {
1012
@PublishedApi internal var _prev = this
1113
@PublishedApi internal var _removed: Boolean = false
1214

13-
public actual inline val nextNode get() = _next
14-
inline actual val prevNode get() = _prev
15-
inline actual val isRemoved get() = _removed
16-
17-
public actual fun addLast(node: Node, permissionsBitmask: Int): Boolean = when (val prev = this._prev) {
15+
fun addLast(node: Node, permissionsBitmask: Int): Boolean = when (val prev = this._prev) {
1816
is ListClosed ->
1917
prev.forbiddenElementsBitmask and permissionsBitmask == 0 && prev.addLast(node, permissionsBitmask)
2018
else -> {
@@ -26,30 +24,26 @@ public actual open class LockFreeLinkedListNode {
2624
}
2725
}
2826

29-
public actual fun close(forbiddenElementsBit: Int) {
30-
addLast(ListClosed(forbiddenElementsBit), forbiddenElementsBit)
31-
}
32-
3327
/*
3428
* Remove that is invoked as a virtual function with a
3529
* potentially augmented behaviour.
3630
* I.g. `LockFreeLinkedListHead` throws, while `SendElementWithUndeliveredHandler`
3731
* invokes handler on remove
3832
*/
39-
public actual open fun remove(): Boolean {
40-
if (_removed) return false
33+
public actual open fun remove() {
34+
if (_removed) return
4135
val prev = this._prev
4236
val next = this._next
4337
prev._next = next
4438
next._prev = prev
4539
_removed = true
46-
return true
4740
}
4841

49-
public actual fun addOneIfEmpty(node: Node): Boolean {
50-
if (_next !== this) return false
51-
addLast(node, Int.MIN_VALUE)
52-
return true
42+
public actual fun attachToList(node: LockFreeLinkedListHead): Node {
43+
if (_next !== this) return _next
44+
val success = addLast(node, Int.MIN_VALUE)
45+
assert { success }
46+
return node
5347
}
5448
}
5549

@@ -58,16 +52,24 @@ public actual open class LockFreeLinkedListHead : Node() {
5852
/**
5953
* Iterates over all elements in this list of a specified type.
6054
*/
61-
public actual inline fun forEach(block: (Node) -> Unit) {
62-
var cur: Node = _next
63-
while (cur != this) {
64-
block(cur)
55+
public actual inline fun forEach(startAfter: LockFreeLinkedListNode?, block: (Node) -> Unit) {
56+
var cur: Node = startAfter ?: this
57+
while (cur._removed) cur = cur._prev // rollback to prev non-removed (or list head)
58+
cur = cur._next
59+
while (cur !== this) {
60+
if (!cur._removed) {
61+
block(cur)
62+
}
6563
cur = cur._next
6664
}
6765
}
6866

67+
public actual fun close(forbiddenElementsBit: Int) {
68+
addLast(ListClosed(forbiddenElementsBit), forbiddenElementsBit)
69+
}
70+
6971
// just a defensive programming -- makes sure that list head sentinel is never removed
70-
public actual final override fun remove(): Nothing = throw UnsupportedOperationException()
72+
public final override fun remove(): Nothing = throw UnsupportedOperationException()
7173
}
7274

7375
private class ListClosed(val forbiddenElementsBitmask: Int): LockFreeLinkedListNode()

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,13 @@ class LinkedListTest {
2020
assertContents(list, 1, 2, 3)
2121
val n4 = IntNode(4).apply { list.addLast(this, Int.MAX_VALUE) }
2222
assertContents(list, 1, 2, 3, 4)
23-
assertTrue(n1.remove())
23+
n1.remove()
2424
assertContents(list, 2, 3, 4)
25-
assertTrue(n3.remove())
25+
n3.remove()
2626
assertContents(list, 2, 4)
27-
assertTrue(n4.remove())
27+
n4.remove()
2828
assertContents(list, 2)
29-
assertTrue(n2.remove())
30-
assertFalse(n2.remove())
29+
n2.remove()
3130
assertContents(list)
3231
}
3332

0 commit comments

Comments
 (0)