Skip to content

Commit 5e88957

Browse files
committed
~
1 parent 32af157 commit 5e88957

File tree

6 files changed

+88
-2
lines changed

6 files changed

+88
-2
lines changed

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

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ internal expect class LocalAtomicInt(value: Int) {
1414
fun get(): Int
1515
fun set(value: Int)
1616
fun decrementAndGet(): Int
17+
fun incrementAndGet(): Int
1718
}
1819

1920
internal inline var LocalAtomicInt.value

kotlinx-coroutines-core/common/src/sync/Mutex.kt

+38-1
Original file line numberDiff line numberDiff line change
@@ -172,13 +172,50 @@ internal open class MutexImpl(locked: Boolean) : SemaphoreImpl(1, if (locked) 1
172172

173173
override fun tryLock(owner: Any?): Boolean =
174174
if (tryAcquire()) {
175-
assert { this.owner.value === NO_OWNER }
175+
val previousOwner = this.owner.value
176+
// First setup the invariant, then fail on assert of things went south
176177
this.owner.value = owner
178+
assert { previousOwner === NO_OWNER }
177179
true
178180
} else {
181+
/*
182+
* We have to ensure invariant that any lock attempt on
183+
* mutex already locked with the same owner fails with ISE.
184+
* Here we can have the following logical race:
185+
* - T1: lock(O1) holds
186+
* - T2: tryLock(O2) fails, we fall into this branch
187+
* - T1: unlock(O1), lock(O3)
188+
* - T2: reads O3 in else branch
189+
*/
190+
check(owner === null || owner !== this.owner.value) {
191+
"Invariant violation: already locked by $owner"
192+
}
179193
false
180194
}
181195

196+
// override fun tryLock(owner: Any?): Boolean {
197+
// /*
198+
// * Here we are maintaining invariant "tryLock(owner) on locked with this very owner mutex throws ISE".
199+
// * In order to achieve that and avoid check-and-act, we do stamped comparison, restarting the operation if
200+
// * stamped check failed.
201+
// */
202+
// while (true) {
203+
// val ownerBeforeAcquisition = this.owner.value
204+
// return if (tryAcquire()) {
205+
// assert { this.owner.value === NO_OWNER }
206+
// this.owner.value = owner
207+
// true
208+
// } else {
209+
// // TODO handle nulls as well and do not retry
210+
// val ownerAfterAcquisition = this.owner.value
211+
// // Owner changed -> we are racing with lock/unlock and cannot deterministically check the invariant, retry
212+
// if (ownerBeforeAcquisition !== ownerAfterAcquisition) continue
213+
// check(owner !== ownerAfterAcquisition) { "Invariant violation: already locked by $owner" }
214+
// false
215+
// }
216+
// }
217+
// }
218+
182219
override fun unlock(owner: Any?) {
183220
while (true) {
184221
// Is this mutex locked?

kotlinx-coroutines-core/common/test/sync/MutexTest.kt

+9
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,13 @@ class MutexTest : TestBase() {
138138
assertTrue(mutex.holdsLock(owner2))
139139
finish(4)
140140
}
141+
142+
@Test
143+
fun testIllegalStateInvariant() = runTest {
144+
val mutex = Mutex()
145+
val owner = Any()
146+
assertTrue(mutex.tryLock(owner))
147+
assertFailsWith<IllegalStateException> { mutex.tryLock(owner) }
148+
assertFailsWith<IllegalStateException> { mutex.lock(owner) }
149+
}
141150
}

kotlinx-coroutines-core/concurrent/test/sync/MutexStressTest.kt

+37-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package kotlinx.coroutines.sync
66

77
import kotlinx.coroutines.*
88
import kotlinx.coroutines.exceptions.*
9+
import kotlinx.coroutines.internal.*
910
import kotlinx.coroutines.selects.*
1011
import kotlin.test.*
1112

@@ -32,7 +33,7 @@ class MutexStressTest : TestBase() {
3233

3334
@Test
3435
fun testMultiThreadedContext() = runTest {
35-
newFixedThreadPoolContext(8, "testMultiThreadedContext").use {
36+
newFixedThreadPoolContext(8, "testMultiThreadedContext").use {
3637
testBody(it)
3738
}
3839
}
@@ -134,4 +135,39 @@ class MutexStressTest : TestBase() {
134135
assertFalse { mutex.isLocked }
135136
}
136137
}
138+
139+
@Test
140+
fun testTryLockInvariant() = runTest {
141+
val mutex = Mutex()
142+
val owner = Any()
143+
val iterations = n * 10 * stressTestMultiplierSqrt
144+
val failures = LocalAtomicInt(0)
145+
val job = launch(Dispatchers.Default) {
146+
repeat(iterations) {
147+
try {
148+
mutex.lock(owner)
149+
yield()
150+
mutex.unlock()
151+
} catch (e: IllegalStateException) {
152+
failures.incrementAndGet()
153+
}
154+
}
155+
}
156+
157+
val job2 = launch(Dispatchers.Default) {
158+
while (job.isActive) {
159+
try {
160+
val locked = mutex.tryLock(owner)
161+
if (locked) {
162+
mutex.unlock(owner)
163+
}
164+
} catch (e: IllegalStateException) {
165+
failures.incrementAndGet()
166+
}
167+
}
168+
}
169+
170+
joinAll(job, job2)
171+
assertTrue(failures.value > 0, "At least one ISE was expected during this stress-test")
172+
}
137173
}

kotlinx-coroutines-core/js/src/internal/LocalAtomics.kt

+1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ internal actual class LocalAtomicInt actual constructor(private var value: Int)
1212
actual fun get(): Int = value
1313

1414
actual fun decrementAndGet(): Int = --value
15+
actual fun incrementAndGet(): Int = ++value
1516
}

kotlinx-coroutines-core/native/src/internal/LocalAtomics.kt

+2
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,6 @@ internal actual class LocalAtomicInt actual constructor(value: Int) {
1717
actual fun get(): Int = iRef.value
1818

1919
actual fun decrementAndGet(): Int = iRef.decrementAndGet()
20+
21+
actual fun incrementAndGet(): Int = iRef.incrementAndGet()
2022
}

0 commit comments

Comments
 (0)