Skip to content

Commit

Permalink
Remove DCSS (#4053)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dkhalanskyjb authored May 21, 2024
1 parent b34dc86 commit 2803a33
Show file tree
Hide file tree
Showing 11 changed files with 496 additions and 233 deletions.
250 changes: 184 additions & 66 deletions kotlinx-coroutines-core/common/src/JobSupport.kt

Large diffs are not rendered by default.

67 changes: 0 additions & 67 deletions kotlinx-coroutines-core/common/src/internal/Atomic.kt

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@

package kotlinx.coroutines.internal

import kotlinx.coroutines.*
import kotlin.jvm.*

/** @suppress **This is unstable API and it is subject to change.** */
public expect open class LockFreeLinkedListNode() {
public val isRemoved: Boolean
public val nextNode: LockFreeLinkedListNode
public val prevNode: LockFreeLinkedListNode
public fun addLast(node: LockFreeLinkedListNode, permissionsBitmask: Int): Boolean
public fun addOneIfEmpty(node: LockFreeLinkedListNode): Boolean
public inline fun addLastIf(node: LockFreeLinkedListNode, crossinline condition: () -> Boolean): Boolean
public open fun remove(): Boolean

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

internal fun LockFreeLinkedListNode.addLast(node: LockFreeLinkedListNode) = addLastIf(node) { true }

/** @suppress **This is unstable API and it is subject to change.** */
public expect open class LockFreeLinkedListHead() : LockFreeLinkedListNode {
public inline fun forEach(block: (LockFreeLinkedListNode) -> Unit)
Expand Down
14 changes: 14 additions & 0 deletions kotlinx-coroutines-core/common/test/JobTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,20 @@ class JobTest : TestBase() {
finish(4)
}

@Test
fun testInvokeOnCancellingFiringOnNormalExit() = runTest {
val job = launch {
expect(2)
}
job.invokeOnCompletion(onCancelling = true) {
assertNull(it)
expect(3)
}
expect(1)
job.join()
finish(4)
}

@Test
fun testOverriddenParent() = runTest {
val parent = Job()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,6 @@ import kotlin.jvm.*

private typealias Node = LockFreeLinkedListNode

@PublishedApi
internal const val UNDECIDED: Int = 0

@PublishedApi
internal const val SUCCESS: Int = 1

@PublishedApi
internal const val FAILURE: Int = 2

@PublishedApi
internal val CONDITION_FALSE: Any = Symbol("CONDITION_FALSE")

/**
* Doubly-linked concurrent list node with remove support.
* Based on paper
Expand Down Expand Up @@ -49,37 +37,10 @@ public actual open class LockFreeLinkedListNode {
private fun removed(): Removed =
_removedRef.value ?: Removed(this).also { _removedRef.lazySet(it) }

@PublishedApi
internal abstract class CondAddOp(
@JvmField val newNode: Node
) : AtomicOp<Node>() {
@JvmField var oldNext: Node? = null

override fun complete(affected: Node, failure: Any?) {
val success = failure == null
val update = if (success) newNode else oldNext
if (update != null && affected._next.compareAndSet( this, update)) {
// only the thread the makes this update actually finishes add operation
if (success) newNode.finishAdd(oldNext!!)
}
}
}

@PublishedApi
internal inline fun makeCondAddOp(node: Node, crossinline condition: () -> Boolean): CondAddOp =
object : CondAddOp(node) {
override fun prepare(affected: Node): Any? = if (condition()) null else CONDITION_FALSE
}

public actual open val isRemoved: Boolean get() = next is Removed

// LINEARIZABLE. Returns Node | Removed
public val next: Any get() {
_next.loop { next ->
if (next !is OpDescriptor) return next
next.perform(this)
}
}
public val next: Any get() = _next.value

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

/**
* Adds last item to this list atomically if the [condition] is true.
* Adds last item to this list. Returns `false` if the list is closed.
*/
public actual inline fun addLastIf(node: Node, crossinline condition: () -> Boolean): Boolean {
val condAdd = makeCondAddOp(node, condition)
public actual fun addLast(node: Node, permissionsBitmask: Int): Boolean {
while (true) { // lock-free loop on prev.next
val prev = prevNode // sentinel node is never removed, so prev is always defined
when (prev.tryCondAddNext(node, this, condAdd)) {
SUCCESS -> return true
FAILURE -> return false
val currentPrev = prevNode
return when {
currentPrev is ListClosed ->
currentPrev.forbiddenElementsBitmask and permissionsBitmask == 0 &&
currentPrev.addLast(node, permissionsBitmask)
currentPrev.addNext(node, this) -> true
else -> continue
}
}
}

// ------ addXXX util ------
/**
* Forbids adding new items to this list.
*/
public actual fun close(forbiddenElementsBit: Int) {
addLast(ListClosed(forbiddenElementsBit), forbiddenElementsBit)
}

/**
* Given:
Expand Down Expand Up @@ -165,17 +133,6 @@ public actual open class LockFreeLinkedListNode {
return true
}

// returns UNDECIDED, SUCCESS or FAILURE
@PublishedApi
internal fun tryCondAddNext(node: Node, next: Node, condAdd: CondAddOp): Int {
node._prev.lazySet(this)
node._next.lazySet(next)
condAdd.oldNext = next
if (!_next.compareAndSet(next, condAdd)) return UNDECIDED
// added operation successfully (linearized) -- complete it & fixup the list
return if (condAdd.perform(this) == null) SUCCESS else FAILURE
}

// ------ removeXXX ------

/**
Expand Down Expand Up @@ -273,10 +230,6 @@ public actual open class LockFreeLinkedListNode {
}
// slow path when we need to help remove operations
this.isRemoved -> return null // nothing to do, this node was removed, bail out asap to save time
prevNext is OpDescriptor -> { // help & retry
prevNext.perform(prev)
return correctPrev() // retry from scratch
}
prevNext is Removed -> {
if (last !== null) {
// newly added (prev) node is already removed, correct last.next around it
Expand Down Expand Up @@ -332,3 +285,5 @@ public actual open class LockFreeLinkedListHead : LockFreeLinkedListNode() {
// optimization: because head is never removed, we don't have to read _next.value to check these:
override val isRemoved: Boolean get() = false
}

private class ListClosed(@JvmField val forbiddenElementsBitmask: Int): LockFreeLinkedListNode()
35 changes: 17 additions & 18 deletions kotlinx-coroutines-core/jsAndWasmShared/src/internal/LinkedList.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,20 @@ public actual open class LockFreeLinkedListNode {
inline actual val prevNode get() = _prev
inline actual val isRemoved get() = _removed

@PublishedApi
internal fun addLast(node: Node) {
val prev = this._prev
node._next = this
node._prev = prev
prev._next = node
this._prev = node
public actual fun addLast(node: Node, permissionsBitmask: Int): Boolean = when (val prev = this._prev) {
is ListClosed ->
prev.forbiddenElementsBitmask and permissionsBitmask == 0 && prev.addLast(node, permissionsBitmask)
else -> {
node._next = this
node._prev = prev
prev._next = node
this._prev = node
true
}
}

public actual fun close(forbiddenElementsBit: Int) {
addLast(ListClosed(forbiddenElementsBit), forbiddenElementsBit)
}

/*
Expand All @@ -30,10 +37,6 @@ public actual open class LockFreeLinkedListNode {
* invokes handler on remove
*/
public actual open fun remove(): Boolean {
return removeImpl()
}

private fun removeImpl(): Boolean {
if (_removed) return false
val prev = this._prev
val next = this._next
Expand All @@ -45,13 +48,7 @@ public actual open class LockFreeLinkedListNode {

public actual fun addOneIfEmpty(node: Node): Boolean {
if (_next !== this) return false
addLast(node)
return true
}

public actual inline fun addLastIf(node: Node, crossinline condition: () -> Boolean): Boolean {
if (!condition()) return false
addLast(node)
addLast(node, Int.MIN_VALUE)
return true
}
}
Expand All @@ -72,3 +69,5 @@ public actual open class LockFreeLinkedListHead : Node() {
// just a defensive programming -- makes sure that list head sentinel is never removed
public actual final override fun remove(): Nothing = throw UnsupportedOperationException()
}

private class ListClosed(val forbiddenElementsBitmask: Int): LockFreeLinkedListNode()
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ class LinkedListTest {
fun testSimpleAddLastRemove() {
val list = LockFreeLinkedListHead()
assertContents(list)
val n1 = IntNode(1).apply { list.addLast(this) }
val n1 = IntNode(1).apply { list.addLast(this, Int.MAX_VALUE) }
assertContents(list, 1)
val n2 = IntNode(2).apply { list.addLast(this) }
val n2 = IntNode(2).apply { list.addLast(this, Int.MAX_VALUE) }
assertContents(list, 1, 2)
val n3 = IntNode(3).apply { list.addLast(this) }
val n3 = IntNode(3).apply { list.addLast(this, Int.MAX_VALUE) }
assertContents(list, 1, 2, 3)
val n4 = IntNode(4).apply { list.addLast(this) }
val n4 = IntNode(4).apply { list.addLast(this, Int.MAX_VALUE) }
assertContents(list, 1, 2, 3, 4)
assertTrue(n1.remove())
assertContents(list, 2, 3, 4)
Expand Down
Loading

0 comments on commit 2803a33

Please sign in to comment.