Skip to content

Enforce mutex contract #2796

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 30 additions & 15 deletions kotlinx-coroutines-core/common/src/sync/Mutex.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import kotlinx.coroutines.internal.*
import kotlinx.coroutines.intrinsics.*
import kotlinx.coroutines.selects.*
import kotlin.contracts.*
import kotlin.coroutines.*
import kotlin.jvm.*
import kotlin.native.concurrent.*

Expand All @@ -20,11 +19,6 @@ import kotlin.native.concurrent.*
* Mutex has two states: _locked_ and _unlocked_.
* It is **non-reentrant**, that is invoking [lock] even from the same thread/coroutine that currently holds
* the lock still suspends the invoker.
*
* JVM API note:
* Memory semantic of the [Mutex] is similar to `synchronized` block on JVM:
* An unlock on a [Mutex] happens-before every subsequent successful lock on that [Mutex].
* Unsuccessful call to [tryLock] do not have any memory effects.
*/
public interface Mutex {
/**
Expand Down Expand Up @@ -81,8 +75,8 @@ public interface Mutex {
* Unlocks this mutex. Throws [IllegalStateException] if invoked on a mutex that is not locked or
* was locked with a different owner token (by identity).
*
* @param owner Optional owner token for debugging. When `owner` is specified (non-null value) and this mutex
* was locked with the different token (by identity), this function throws [IllegalStateException].
* @param owner Optional owner token for debugging. When `owner` does not match the owner (or its absence)
* that locked this mutex, this function throws [IllegalStateException].
*/
public fun unlock(owner: Any? = null)
}
Expand Down Expand Up @@ -136,6 +130,12 @@ private val EMPTY_LOCKED = Empty(LOCKED)
private val EMPTY_UNLOCKED = Empty(UNLOCKED)

private class Empty(
/*
* State of the lock:
* - LOCKED
* - UNLOCKED
* - owner (of the lock, passed via 'lock(owner)')
*/
@JvmField val locked: Any
) {
override fun toString(): String = "Empty[$locked]"
Expand Down Expand Up @@ -163,7 +163,7 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2<Any?, Mutex> {
return state is LockedQueue && state.isEmpty
}

public override fun tryLock(owner: Any?): Boolean {
override fun tryLock(owner: Any?): Boolean {
_state.loop { state ->
when (state) {
is Empty -> {
Expand All @@ -183,7 +183,7 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2<Any?, Mutex> {
}
}

public override suspend fun lock(owner: Any?) {
override suspend fun lock(owner: Any?) {
// fast-path -- try lock
if (tryLock(owner)) return
// slow-path -- suspend
Expand Down Expand Up @@ -304,16 +304,23 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2<Any?, Mutex> {
_state.loop { state ->
when (state) {
is Empty -> {
if (owner == null)
if (owner == null) {
check(state.locked !== UNLOCKED) { "Mutex is not locked" }
else
check(state.locked === owner) { "Mutex is locked by ${state.locked} but expected $owner" }
expectNoOwner(state.locked)
}
else {
val actualOwner = state.locked
expectOwner(actualOwner, owner)
}
if (_state.compareAndSet(state, EMPTY_UNLOCKED)) return
}
is OpDescriptor -> state.perform(this)
is LockedQueue -> {
if (owner != null)
check(state.owner === owner) { "Mutex is locked by ${state.owner} but expected $owner" }
if (owner != null) {
expectOwner(state.owner, owner)
} else {
expectNoOwner(state.owner)
}
val waiter = state.removeFirstOrNull()
if (waiter == null) {
val op = UnlockOp(state)
Expand All @@ -332,6 +339,14 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2<Any?, Mutex> {
}
}

private fun expectNoOwner(l: Any) {
check(l == LOCKED) { "Mutex is locked by $l but expected no owner" }
}

private fun expectOwner(actualOwner: Any, owner: Any?) {
check(actualOwner === owner) { "Mutex is locked by $actualOwner but expected $owner" }
}

override fun toString(): String {
_state.loop { state ->
when (state) {
Expand Down
59 changes: 39 additions & 20 deletions kotlinx-coroutines-core/common/test/sync/MutexTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

package kotlinx.coroutines.sync

import kotlinx.atomicfu.*
import kotlinx.coroutines.*
import kotlin.test.*

Expand Down Expand Up @@ -32,7 +31,7 @@ class MutexTest : TestBase() {
}

@Test
fun tryLockTest() {
fun testTryLock() {
val mutex = Mutex()
assertFalse(mutex.isLocked)
assertTrue(mutex.tryLock())
Expand All @@ -50,7 +49,7 @@ class MutexTest : TestBase() {
}

@Test
fun withLockTest() = runTest {
fun testWithLock() = runTest {
val mutex = Mutex()
assertFalse(mutex.isLocked)
mutex.withLock {
Expand All @@ -60,23 +59,7 @@ class MutexTest : TestBase() {
}

@Test
fun testUnconfinedStackOverflow() {
val waiters = 10000
val mutex = Mutex(true)
var done = 0
repeat(waiters) {
GlobalScope.launch(Dispatchers.Unconfined) { // a lot of unconfined waiters
mutex.withLock {
done++
}
}
}
mutex.unlock() // should not produce StackOverflowError
assertEquals(waiters, done)
}

@Test
fun holdLock() = runTest {
fun testHoldsLock() = runTest {
val mutex = Mutex()
val firstOwner = Any()
val secondOwner = Any()
Expand Down Expand Up @@ -107,4 +90,40 @@ class MutexTest : TestBase() {
assertFalse(mutex.holdsLock(firstOwner))
assertFalse(mutex.holdsLock(secondOwner))
}

@Test
fun testUnlockWithoutOwner() {
val owner = Any()
val mutex = Mutex()
assertTrue(mutex.tryLock(owner))
assertFailsWith<IllegalStateException> { mutex.unlock(Any()) }
assertFailsWith<IllegalStateException> { mutex.unlock() }
assertFailsWith<IllegalStateException> { mutex.unlock(null) }
assertTrue(mutex.holdsLock(owner))
mutex.unlock(owner)
assertFalse(mutex.isLocked)
}

@Test
fun testUnlockWithoutOwnerWithLockedQueue() = runTest {
val owner = Any()
val owner2 = Any()
val mutex = Mutex()
assertTrue(mutex.tryLock(owner))
expect(1)
launch {
expect(2)
mutex.lock(owner2)
}
yield()
expect(3)
assertFailsWith<IllegalStateException> { mutex.unlock(owner2) }
assertFailsWith<IllegalStateException> { mutex.unlock() }
assertFailsWith<IllegalStateException> { mutex.unlock(null) }
assertTrue(mutex.holdsLock(owner))
mutex.unlock(owner)
assertTrue(mutex.isLocked)
assertTrue(mutex.holdsLock(owner2))
finish(4)
}
}