diff --git a/atomicfu/api/atomicfu.api b/atomicfu/api/atomicfu.api index f6fbbcab..4b6a68f2 100644 --- a/atomicfu/api/atomicfu.api +++ b/atomicfu/api/atomicfu.api @@ -135,6 +135,20 @@ public final class kotlinx/atomicfu/TraceKt { public static final fun named (Lkotlinx/atomicfu/TraceBase;Ljava/lang/String;)Lkotlinx/atomicfu/TraceBase; } +public final class kotlinx/atomicfu/locks/Mutex { + public fun ()V + public fun (Ljava/util/concurrent/locks/ReentrantLock;)V + public final fun getReentrantLock ()Ljava/util/concurrent/locks/ReentrantLock; + public final fun isLocked ()Z + public final fun lock ()V + public final fun tryLock ()Z + public final fun unlock ()V +} + +public final class kotlinx/atomicfu/locks/MutexKt { + public static final fun withLock (Lkotlinx/atomicfu/locks/Mutex;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; +} + public final class kotlinx/atomicfu/parking/KThread { public static final field Companion Lkotlinx/atomicfu/parking/KThread$Companion; } diff --git a/atomicfu/build.gradle.kts b/atomicfu/build.gradle.kts index 1cab94b0..a001746e 100644 --- a/atomicfu/build.gradle.kts +++ b/atomicfu/build.gradle.kts @@ -88,6 +88,7 @@ kotlin { jvmTest { dependencies { + implementation("org.jetbrains.kotlinx:lincheck:2.35") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-test") implementation("org.jetbrains.kotlin:kotlin-test-junit") diff --git a/atomicfu/src/androidNative32BitMain/kotlin/kotlinx/atomicfu/locks/NativeMutexNode.kt b/atomicfu/src/androidNative32BitMain/kotlin/kotlinx/atomicfu/locks/NativeMutexNode.kt deleted file mode 100644 index 5b6ff7cf..00000000 --- a/atomicfu/src/androidNative32BitMain/kotlin/kotlinx/atomicfu/locks/NativeMutexNode.kt +++ /dev/null @@ -1,31 +0,0 @@ -package kotlinx.atomicfu.locks - -import kotlinx.cinterop.* -import platform.posix.* -import kotlin.concurrent.Volatile - -public actual class NativeMutexNode { - - @Volatile - private var isLocked = false - private val pMutex = nativeHeap.alloc().apply { pthread_mutex_init(ptr, null) } - private val pCond = nativeHeap.alloc().apply { pthread_cond_init(ptr, null) } - - internal actual var next: NativeMutexNode? = null - - actual fun lock() { - pthread_mutex_lock(pMutex.ptr) - while (isLocked) { // wait till locked are available - pthread_cond_wait(pCond.ptr, pMutex.ptr) - } - isLocked = true - pthread_mutex_unlock(pMutex.ptr) - } - - actual fun unlock() { - pthread_mutex_lock(pMutex.ptr) - isLocked = false - pthread_cond_broadcast(pCond.ptr) - pthread_mutex_unlock(pMutex.ptr) - } -} \ No newline at end of file diff --git a/atomicfu/src/androidNative64BitMain/kotlin/kotlinx/atomicfu/locks/NativeMutexNode.kt b/atomicfu/src/androidNative64BitMain/kotlin/kotlinx/atomicfu/locks/NativeMutexNode.kt deleted file mode 100644 index 5b6ff7cf..00000000 --- a/atomicfu/src/androidNative64BitMain/kotlin/kotlinx/atomicfu/locks/NativeMutexNode.kt +++ /dev/null @@ -1,31 +0,0 @@ -package kotlinx.atomicfu.locks - -import kotlinx.cinterop.* -import platform.posix.* -import kotlin.concurrent.Volatile - -public actual class NativeMutexNode { - - @Volatile - private var isLocked = false - private val pMutex = nativeHeap.alloc().apply { pthread_mutex_init(ptr, null) } - private val pCond = nativeHeap.alloc().apply { pthread_cond_init(ptr, null) } - - internal actual var next: NativeMutexNode? = null - - actual fun lock() { - pthread_mutex_lock(pMutex.ptr) - while (isLocked) { // wait till locked are available - pthread_cond_wait(pCond.ptr, pMutex.ptr) - } - isLocked = true - pthread_mutex_unlock(pMutex.ptr) - } - - actual fun unlock() { - pthread_mutex_lock(pMutex.ptr) - isLocked = false - pthread_cond_broadcast(pCond.ptr) - pthread_mutex_unlock(pMutex.ptr) - } -} \ No newline at end of file diff --git a/atomicfu/src/commonMain/kotlin/kotlinx/atomicfu/locks/Mutex.kt b/atomicfu/src/commonMain/kotlin/kotlinx/atomicfu/locks/Mutex.kt new file mode 100644 index 00000000..5b8a483e --- /dev/null +++ b/atomicfu/src/commonMain/kotlin/kotlinx/atomicfu/locks/Mutex.kt @@ -0,0 +1,79 @@ +package kotlinx.atomicfu.locks + +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * Mutual exclusion for Kotlin Multiplatform. + * + * It can protect a shared resource or critical section from multiple thread accesses. + * Threads can acquire the lock by calling [lock] and release the lock by calling [unlock]. + * + * When a thread calls [lock] while another thread is locked, it will suspend until the lock is released. + * When multiple threads are waiting for the lock, they will acquire it in a fair order (first in first out). + * + * It is reentrant, meaning the lock holding thread can call [lock] multiple times without suspending. + * To release the lock (after multiple [lock] calls) an equal number of [unlock] calls are required. + * + * This Mutex should not be used in combination with coroutines and `suspend` functions + * as it blocks the waiting thread. + * Use the `Mutex` from the coroutines library instead. + * + * ```Kotlin + * mutex.withLock { + * // Critical section only executed by + * // one thread at a time. + * } + * ``` + */ +expect class Mutex() { + /** + * Returns `true` if this mutex is locked. + */ + fun isLocked(): Boolean + + /** + * Tries to lock this mutex, returning `false` if this mutex is already locked. + * + * It is recommended to use [withLock] for safety reasons, so that the acquired lock is always + * released at the end of your critical section, and [unlock] is never invoked before a successful + * lock acquisition. + */ + fun tryLock(): Boolean + + /** + * Locks the mutex, suspends the thread until the lock is acquired. + * + * It is recommended to use [withLock] for safety reasons, so that the acquired lock is always + * released at the end of your critical section, and [unlock] is never invoked before a successful + * lock acquisition. + */ + fun lock() + + /** + * Releases the lock. + * Throws [IllegalStateException] when the current thread is not holding the lock. + * + * It is recommended to use [withLock] for safety reasons, so that the acquired lock is always + * released at the end of the critical section, and [unlock] is never invoked before a successful + * lock acquisition. + */ + fun unlock() +} + +/** + * Executes the given code [block] under this mutex's lock. + * + * @return result of [block] + */ +@OptIn(ExperimentalContracts::class) +inline fun Mutex.withLock(block: () -> T): T { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + lock() + return try { + block() + } finally { + unlock() + } +} diff --git a/atomicfu/src/concurrentMain/kotlin/kotlinx/atomicfu/locks/Mutex.kt b/atomicfu/src/concurrentMain/kotlin/kotlinx/atomicfu/locks/Mutex.kt new file mode 100644 index 00000000..ccde8fe5 --- /dev/null +++ b/atomicfu/src/concurrentMain/kotlin/kotlinx/atomicfu/locks/Mutex.kt @@ -0,0 +1,150 @@ +package kotlinx.atomicfu.locks + +import kotlinx.atomicfu.AtomicRef +import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.parking.ThreadParker +import kotlinx.atomicfu.parking.currentThreadId + +internal class NativeMutex { + /** + * Mutex implementation for Kotlin/Native. + * In concurrentMain sourceSet to be testable with Lincheck. + * + * The [state] variable stands for: 0 -> Lock is free + * 1 -> Lock is locked but no waiters + * 4 -> Lock is locked with 3 waiters + * + * The state.incrementAndGet() call makes my claim on the lock. + * The returned value either means I acquired it (when it is 1). + * Or I need to enqueue and park (when it is > 1). + * + * The [holdCount] variable is to enable reentrancy. + * + * Works by using a [parkingQueue]. + * When a thread tries to acquire the lock, but finds it is already locked it enqueues by appending to the [parkingQueue]. + * On enqueue the parking queue provides the second last node, this node is used to park on. + * When our thread is woken up that means that the thread parked on the thrid last node called unpark on the second last node. + * Since a woken up thread is first inline it means that it's node is the head and can therefore dequeue. + * + * Unlocking happens by calling state.decrementAndGet(). + * When the returned value is 0 it means the lock is free and we can simply return. + * If the new state is > 0, then there are waiters. We wake up the first by unparking the head of the queue. + * This even works when a thread is not parked yet, + * since the ThreadParker can be pre-unparked resulting in the parking call to return immediately. + */ + private val parkingQueue = ParkingQueue() + private val owningThread = atomic(-1L) + private val state = atomic(0) + private val holdCount = atomic(0) + + + fun lock() { + val currentThreadId = currentThreadId() + + // Has to be checked in this order! + if (holdCount.value > 0 && currentThreadId == owningThread.value) { + // Is reentring thread + holdCount.incrementAndGet() + return + } + + // Otherwise try acquire lock + val newState = state.incrementAndGet() + // If new state 1 than I have acquired lock skipping queue. + if (newState == 1) { + owningThread.value = currentThreadId + holdCount.incrementAndGet() + return + } + + // If state larger than 1 -> enqueue and park + // When woken up thread has acquired lock and his node in the queue is therefore at the head. + // Remove head + if (newState > 1) { + val prevNode = parkingQueue.enqueue() + prevNode.parker.park() + parkingQueue.dequeue() + owningThread.value = currentThreadId + holdCount.incrementAndGet() + return + } + } + + fun unlock() { + val currentThreadId = currentThreadId() + val currentOwnerId = owningThread.value + if (currentThreadId != currentOwnerId) throw IllegalStateException("Thread is not holding the lock") + + // dec hold count + val newHoldCount = holdCount.decrementAndGet() + if (newHoldCount > 0) return + if (newHoldCount < 0) throw IllegalStateException("Thread unlocked more than it locked") + + // Lock is released by decrementing (only if decremented to 0) + val currentState = state.decrementAndGet() + if (currentState == 0) return + + // If waiters wake up the first in line. The woken up thread will dequeue the node. + if (currentState > 0) { + val nextParker = parkingQueue.getHead() + nextParker.parker.unpark() + return + } + } + + fun isLocked(): Boolean { + return state.value > 0 + } + + fun tryLock(): Boolean { + val currentThreadId = currentThreadId() + if (holdCount.value > 0 && owningThread.value == currentThreadId || state.compareAndSet(0, 1)) { + owningThread.value = currentThreadId + holdCount.incrementAndGet() + return true + } + return false + } + + // Based on Micheal-Scott Queue + private class ParkingQueue { + private val head: AtomicRef + private val tail: AtomicRef + + init { + val first = Node() + head = atomic(first) + tail = atomic(first) + } + + fun getHead(): Node { + return head.value + } + + fun enqueue(): Node { + while (true) { + val node = Node() + val curTail = tail.value + if (curTail.next.compareAndSet(null, node)) { + tail.compareAndSet(curTail, node) + return curTail + } + else tail.compareAndSet(curTail, curTail.next.value!!) + } + } + + fun dequeue() { + while (true) { + val currentHead = head.value + val currentHeadNext = currentHead.next.value ?: throw IllegalStateException("Dequeing parker but already empty, should not be possible") + if (head.compareAndSet(currentHead, currentHeadNext)) return + } + } + + } + + private class Node { + val parker = ThreadParker() + val next = atomic(null) + } +} diff --git a/atomicfu/src/concurrentTest/kotlin/kotlinx/atomicfu/locks/NativeMutexTest.kt b/atomicfu/src/concurrentTest/kotlin/kotlinx/atomicfu/locks/NativeMutexTest.kt new file mode 100644 index 00000000..9297f371 --- /dev/null +++ b/atomicfu/src/concurrentTest/kotlin/kotlinx/atomicfu/locks/NativeMutexTest.kt @@ -0,0 +1,90 @@ +package kotlinx.atomicfu.locks + +import kotlinx.atomicfu.parking.sleepMills +import kotlinx.atomicfu.parking.testThread +import kotlin.test.Test +import kotlin.test.assertEquals + +class NativeMutexTest { + + + @Test + fun testNativeMutexSlow() { + val mutex = NativeMutex() + val resultList = mutableListOf() + + val fut1 = testThread { + repeat(30) { i -> + mutex.lock() + resultList.add("a$i") + sleepMills(100) + resultList.add("a$i") + mutex.unlock() + } + } + + val fut2 = testThread { + repeat(30) { i -> + mutex.lock() + resultList.add("b$i") + sleepMills(100) + resultList.add("b$i") + mutex.unlock() + } + } + + repeat(30) { i -> + mutex.lock() + resultList.add("c$i") + sleepMills(100) + resultList.add("c$i") + mutex.unlock() + } + fut1.join() + fut2.join() + + resultList.filterIndexed { i, _ -> i % 2 == 0 } + .zip(resultList.filterIndexed {i, _ -> i % 2 == 1}) { a, b -> + assertEquals(a, b) + } + } + + @Test + fun testNativeMutexFast() { + val mutex = Mutex() + val resultList = mutableListOf() + + val fut1 = testThread { + repeat(30000) { i -> + mutex.lock() + resultList.add("a$i") + resultList.add("a$i") + mutex.unlock() + } + } + + val fut2 = testThread { + repeat(30000) { i -> + mutex.lock() + resultList.add("b$i") + resultList.add("b$i") + mutex.unlock() + } + } + + repeat(30000) { i -> + mutex.lock() + resultList.add("c$i") + resultList.add("c$i") + mutex.unlock() + } + fut1.join() + fut2.join() + + resultList + .filterIndexed { i, _ -> i % 2 == 0 } + .zip(resultList.filterIndexed {i, _ -> i % 2 == 1}) { a, b -> + assertEquals(a, b) + } + } +} \ No newline at end of file diff --git a/atomicfu/src/concurrentTest/kotlin/kotlinx/atomicfu/locks/ReentrancyTests.kt b/atomicfu/src/concurrentTest/kotlin/kotlinx/atomicfu/locks/ReentrancyTests.kt new file mode 100644 index 00000000..28fe7c78 --- /dev/null +++ b/atomicfu/src/concurrentTest/kotlin/kotlinx/atomicfu/locks/ReentrancyTests.kt @@ -0,0 +1,28 @@ +package kotlinx.atomicfu.locks + +import kotlin.test.Test +import kotlin.test.assertFails + +class ReentrancyTests { + + @Test + fun reentrantTestSuccess() { + val lock = NativeMutex() + lock.lock() + lock.lock() + lock.unlock() + lock.unlock() + } + + @Test + fun reentrantTestFail() { + val lock = NativeMutex() + lock.lock() + lock.lock() + lock.unlock() + lock.unlock() + assertFails { + lock.unlock() + } + } +} \ No newline at end of file diff --git a/atomicfu/src/concurrentTest/kotlin/kotlinx/atomicfu/locks/VaryingContentionTest.kt b/atomicfu/src/concurrentTest/kotlin/kotlinx/atomicfu/locks/VaryingContentionTest.kt new file mode 100644 index 00000000..8e8beb97 --- /dev/null +++ b/atomicfu/src/concurrentTest/kotlin/kotlinx/atomicfu/locks/VaryingContentionTest.kt @@ -0,0 +1,69 @@ +package kotlinx.atomicfu.locks + +import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.parking.Fut +import kotlin.test.Test +import kotlin.test.assertTrue + +class VaryingContentionTest { + + @Test + fun varyingContentionTest() { + val lockInt = LockInt() + multiTestLock(lockInt, 10, 100000) + println("Varying Contention Test 1") + multiTestLock(lockInt, 1, 200000) + println("Varying Contention Test 2") + multiTestLock(lockInt, 20, 300000) + println("Varying Contention Test 3") + multiTestLock(lockInt, 1, 400000) + println("Varying Contention Test 4") + multiTestLock(lockInt, 2, 1000000) + println("Varying Contention Test Done") + } + + + private fun multiTestLock(lockInt: LockInt, nThreads: Int, countTo: Int) { + val futureList = mutableListOf() + repeat(nThreads) { i -> + val test = LockIntTest(lockInt, countTo, nThreads, i) + futureList.add(testWithThread(test)) + } + Fut.waitAllAndThrow(futureList) + } + + private fun testWithThread(t: LockIntTest): Fut { + return Fut { + while (true) { + t.lockInt.lock() + if (t.lockInt.n % t.mod == t.id) t.lockInt.n++ + if (t.lockInt.n >= t.max) { + t.lockInt.unlock() + break + } + t.lockInt.unlock() + } + } + } + + data class LockIntTest( + val lockInt: LockInt, + val max: Int, + val mod: Int, + val id: Int, + ) + + class LockInt { + private val lock = NativeMutex() + private val check = atomic(0) + var n = 0 + fun lock() { + lock.lock() + assertTrue(check.incrementAndGet() == 1) + } + fun unlock() { + assertTrue(check.decrementAndGet() == 0) + lock.unlock() + } + } +} \ No newline at end of file diff --git a/atomicfu/src/concurrentTest/kotlin/kotlinx/atomicfu/parking/TestThread.kt b/atomicfu/src/concurrentTest/kotlin/kotlinx/atomicfu/parking/TestThread.kt index ca43c4a7..19caaea3 100644 --- a/atomicfu/src/concurrentTest/kotlin/kotlinx/atomicfu/parking/TestThread.kt +++ b/atomicfu/src/concurrentTest/kotlin/kotlinx/atomicfu/parking/TestThread.kt @@ -1,9 +1,44 @@ package kotlinx.atomicfu.parking +import kotlinx.atomicfu.atomic + internal fun testThread(doConcurrent: () -> Unit): TestThread = TestThread { doConcurrent() } internal expect class TestThread(toDo: () -> Unit) { fun join() } -expect fun sleepMills(millis: Long) \ No newline at end of file +internal expect fun sleepMills(millis: Long) + +internal class Fut(private val block: () -> Unit) { + private var thread: TestThread? = null + private val atomicError = atomic(null) + val done = atomic(false) + init { + val th = testThread { + try { block() } + catch (t: Throwable) { + atomicError.value = t + throw t + } + finally { done.value = true } + } + thread = th + } + fun waitThrowing() { + thread!!.join() + throwIfError() + } + + fun throwIfError() = atomicError.value?.let { throw it } + + companion object { + fun waitAllAndThrow(futs: List) { + while(futs.any { !it.done.value }) { + sleepMills(1000) + futs.forEach { it.throwIfError() } + } + } + } + +} diff --git a/atomicfu/src/concurrentTest/kotlin/kotlinx/atomicfu/parking/TimedParkingTest.kt b/atomicfu/src/concurrentTest/kotlin/kotlinx/atomicfu/parking/TimedParkingTest.kt index fe7d754d..7e70fbaa 100644 --- a/atomicfu/src/concurrentTest/kotlin/kotlinx/atomicfu/parking/TimedParkingTest.kt +++ b/atomicfu/src/concurrentTest/kotlin/kotlinx/atomicfu/parking/TimedParkingTest.kt @@ -1,6 +1,5 @@ package kotlinx.atomicfu.parking -import kotlinx.atomicfu.atomic import kotlin.test.Test import kotlin.test.assertTrue import kotlin.time.measureTime @@ -144,35 +143,3 @@ class TimedParkingTest { } -internal class Fut(private val block: () -> Unit) { - private var thread: TestThread? = null - private val atomicError = atomic(null) - val done = atomic(false) - init { - val th = testThread { - try { block() } - catch (t: Throwable) { - atomicError.value = t - throw t - } - finally { done.value = true } - } - thread = th - } - fun waitThrowing() { - thread!!.join() - throwIfError() - } - - fun throwIfError() = atomicError.value?.let { throw it } - - companion object { - fun waitAllAndThrow(futs: List) { - while(futs.any { !it.done.value }) { - sleepMills(1000) - futs.forEach { it.throwIfError() } - } - } - } - -} diff --git a/atomicfu/src/jsAndWasmSharedMain/kotlin/kotlinx/atomicfu/locks/Mutex.jsAndWasmShared.kt b/atomicfu/src/jsAndWasmSharedMain/kotlin/kotlinx/atomicfu/locks/Mutex.jsAndWasmShared.kt new file mode 100644 index 00000000..cba1b6dd --- /dev/null +++ b/atomicfu/src/jsAndWasmSharedMain/kotlin/kotlinx/atomicfu/locks/Mutex.jsAndWasmShared.kt @@ -0,0 +1,15 @@ +package kotlinx.atomicfu.locks + +/** + * Part of multiplatform mutex. + * Since this mutex will run in a single threaded environment, it doesn't provide any real synchronization. + * + * It does keep track of reentrancy. + */ +actual class Mutex { + private var state = 0 + actual fun isLocked(): Boolean = state != 0 + actual fun tryLock(): Boolean = true + actual fun lock(): Unit { state++ } + actual fun unlock(): Unit { if (state-- < 0) throw IllegalStateException("Mutex already unlocked") } +} \ No newline at end of file diff --git a/atomicfu/src/jvmMain/kotlin/kotlinx/atomicfu/locks/Mutex.kt b/atomicfu/src/jvmMain/kotlin/kotlinx/atomicfu/locks/Mutex.kt new file mode 100644 index 00000000..476abd69 --- /dev/null +++ b/atomicfu/src/jvmMain/kotlin/kotlinx/atomicfu/locks/Mutex.kt @@ -0,0 +1,20 @@ +package kotlinx.atomicfu.locks + +/** + * This mutex uses a [ReentrantLock]. + * + * [getReentrantLock] obtains the actual [ReentrantLock]. + * Construct with `Mutex(reentrantLock)` to create a [Mutex] that uses an existing instance of [ReentrantLock]. + */ +actual class Mutex(private val reentrantLock: java.util.concurrent.locks.ReentrantLock) { + actual constructor(): this(ReentrantLock()) + actual fun isLocked(): Boolean = reentrantLock.isLocked + actual fun tryLock(): Boolean = reentrantLock.tryLock() + actual fun lock() = reentrantLock.lock() + actual fun unlock() = reentrantLock.unlock() + + /** + * @return the underlying [ReentrantLock] + */ + fun getReentrantLock(): ReentrantLock = reentrantLock +} \ No newline at end of file diff --git a/atomicfu/src/jvmTest/kotlin/kotlinx/atomicfu/locks/NativeMutexLincheckReentrantTest.kt b/atomicfu/src/jvmTest/kotlin/kotlinx/atomicfu/locks/NativeMutexLincheckReentrantTest.kt new file mode 100644 index 00000000..f1ee08c3 --- /dev/null +++ b/atomicfu/src/jvmTest/kotlin/kotlinx/atomicfu/locks/NativeMutexLincheckReentrantTest.kt @@ -0,0 +1,54 @@ +import kotlinx.atomicfu.locks.NativeMutex +import org.jetbrains.kotlinx.lincheck.LoggingLevel +import org.jetbrains.kotlinx.lincheck.annotations.Operation +import org.jetbrains.kotlinx.lincheck.check +import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.ModelCheckingOptions +import kotlin.test.Test + +class NativeMutexLincheckReentrantTest { + class Counter { + @Volatile + private var value = 0 + + fun inc(): Int = ++value + fun get() = value + } + private val lock = NativeMutex() + private val counter = Counter() + + @Test + fun modelCheckingTest(): Unit = ModelCheckingOptions() + .iterations(2) // Change to 300 for exhaustive testing + .invocationsPerIteration(5_000) + .actorsBefore(1) + .threads(3) + .actorsPerThread(3) + .actorsAfter(0) + .hangingDetectionThreshold(100) + .logLevel(LoggingLevel.INFO) + .check(this::class.java) + + @Operation + fun inc(): Int { + lock.lock() + if (!lock.tryLock()) throw IllegalStateException("couldnt reent with trylock") + if (!lock.tryLock()) throw IllegalStateException("couldnt reent with trylock") + val result = counter.inc() + lock.unlock() + lock.unlock() + lock.unlock() + return result + } + + @Operation + fun get(): Int { + lock.lock() + if (!lock.tryLock()) throw IllegalStateException("couldnt reent with trylock") + if (!lock.tryLock()) throw IllegalStateException("couldnt reent with trylock") + val result = counter.get() + lock.unlock() + lock.unlock() + lock.unlock() + return result + } +} diff --git a/atomicfu/src/jvmTest/kotlin/kotlinx/atomicfu/locks/NativeMutexLincheckTest.kt b/atomicfu/src/jvmTest/kotlin/kotlinx/atomicfu/locks/NativeMutexLincheckTest.kt new file mode 100644 index 00000000..f2482a1d --- /dev/null +++ b/atomicfu/src/jvmTest/kotlin/kotlinx/atomicfu/locks/NativeMutexLincheckTest.kt @@ -0,0 +1,44 @@ +import kotlinx.atomicfu.locks.NativeMutex +import org.jetbrains.kotlinx.lincheck.LoggingLevel +import org.jetbrains.kotlinx.lincheck.annotations.Operation +import org.jetbrains.kotlinx.lincheck.check +import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.ModelCheckingOptions +import kotlin.test.Test + +class NativeMutexLincheckTest { + class Counter { + @Volatile + private var value = 0 + + fun inc(): Int = ++value + fun get() = value + } + private val lock = NativeMutex() + private val counter = Counter() + + @Test + fun modelCheckingTest(): Unit = ModelCheckingOptions() + .iterations(2) // Change to 300 for exhaustive testing + .invocationsPerIteration(5_000) + .actorsBefore(1) + .threads(3) + .actorsPerThread(3) + .actorsAfter(0) + .hangingDetectionThreshold(100) + .logLevel(LoggingLevel.INFO) + .check(this::class.java) + + @Operation + fun inc() { + lock.lock() + counter.inc() + lock.unlock() + } + + @Operation + fun get() { + lock.lock() + counter.get() + lock.unlock() + } +} \ No newline at end of file diff --git a/atomicfu/src/mingwMain/kotlin/kotlinx/atomicfu/locks/NativeMutexNode.kt b/atomicfu/src/mingwMain/kotlin/kotlinx/atomicfu/locks/NativeMutexNode.kt deleted file mode 100644 index 4c23f564..00000000 --- a/atomicfu/src/mingwMain/kotlin/kotlinx/atomicfu/locks/NativeMutexNode.kt +++ /dev/null @@ -1,31 +0,0 @@ -package kotlinx.atomicfu.locks - -import kotlinx.cinterop.* -import platform.posix.* -import kotlin.concurrent.Volatile - -public actual class NativeMutexNode { - - @Volatile - private var isLocked = false - private val pMutex = nativeHeap.alloc().apply { pthread_mutex_init(ptr, null) } - private val pCond = nativeHeap.alloc().apply { pthread_cond_init(ptr, null) } - - internal actual var next: NativeMutexNode? = null - - actual fun lock() { - pthread_mutex_lock(pMutex.ptr) - while (isLocked) { // wait till locked are available - pthread_cond_wait(pCond.ptr, pMutex.ptr) - } - isLocked = true - pthread_mutex_unlock(pMutex.ptr) - } - - actual fun unlock() { - pthread_mutex_lock(pMutex.ptr) - isLocked = false - pthread_cond_broadcast(pCond.ptr) - pthread_mutex_unlock(pMutex.ptr) - } -} \ No newline at end of file diff --git a/atomicfu/src/nativeMain/kotlin/kotlinx/atomicfu/locks/Mutex.kt b/atomicfu/src/nativeMain/kotlin/kotlinx/atomicfu/locks/Mutex.kt new file mode 100644 index 00000000..f38f60e6 --- /dev/null +++ b/atomicfu/src/nativeMain/kotlin/kotlinx/atomicfu/locks/Mutex.kt @@ -0,0 +1,9 @@ +package kotlinx.atomicfu.locks + +actual class Mutex { + private val lock = NativeMutex() + actual fun isLocked() = lock.isLocked() + actual fun tryLock() = lock.tryLock() + actual fun lock() = lock.lock() + actual fun unlock() = lock.unlock() +} \ No newline at end of file diff --git a/atomicfu/src/nativeMain/kotlin/kotlinx/atomicfu/locks/NativeMutexNode.kt b/atomicfu/src/nativeMain/kotlin/kotlinx/atomicfu/locks/NativeMutexNode.kt deleted file mode 100644 index f2c72e16..00000000 --- a/atomicfu/src/nativeMain/kotlin/kotlinx/atomicfu/locks/NativeMutexNode.kt +++ /dev/null @@ -1,9 +0,0 @@ -package kotlinx.atomicfu.locks - -public expect class NativeMutexNode() { - internal var next: NativeMutexNode? - - public fun lock() - - public fun unlock() -} \ No newline at end of file diff --git a/atomicfu/src/nativeMain/kotlin/kotlinx/atomicfu/locks/Synchronized.kt b/atomicfu/src/nativeMain/kotlin/kotlinx/atomicfu/locks/Synchronized.kt index 3fdfc38c..9729d620 100644 --- a/atomicfu/src/nativeMain/kotlin/kotlinx/atomicfu/locks/Synchronized.kt +++ b/atomicfu/src/nativeMain/kotlin/kotlinx/atomicfu/locks/Synchronized.kt @@ -1,159 +1,11 @@ package kotlinx.atomicfu.locks -import platform.posix.* -import kotlinx.atomicfu.locks.SynchronizedObject.Status.* -import kotlinx.cinterop.UnsafeNumber -import kotlin.concurrent.AtomicReference - -@OptIn(UnsafeNumber::class) // required for KT-60572 public actual open class SynchronizedObject { - - protected val lock = AtomicReference(LockState(UNLOCKED, 0, 0)) - - public fun lock() { - val currentThreadId = pthread_self()!! - while (true) { - val state = lock.value - when (state.status) { - UNLOCKED -> { - val thinLock = LockState(THIN, 1, 0, currentThreadId) - if (lock.compareAndSet(state, thinLock)) - return - } - - THIN -> { - if (currentThreadId == state.ownerThreadId) { - // reentrant lock - val thinNested = LockState(THIN, state.nestedLocks + 1, state.waiters, currentThreadId) - if (lock.compareAndSet(state, thinNested)) - return - } else { - // another thread is trying to take this lock -> allocate native mutex - val mutex = mutexPool.allocate() - mutex.lock() - val fatLock = LockState(FAT, state.nestedLocks, state.waiters + 1, state.ownerThreadId, mutex) - if (lock.compareAndSet(state, fatLock)) { - //block the current thread waiting for the owner thread to release the permit - mutex.lock() - tryLockAfterResume(currentThreadId) - return - } else { - // return permit taken for the owner thread and release mutex back to the pool - mutex.unlock() - mutexPool.release(mutex) - } - } - } - - FAT -> { - if (currentThreadId == state.ownerThreadId) { - // reentrant lock - val nestedFatLock = - LockState(FAT, state.nestedLocks + 1, state.waiters, state.ownerThreadId, state.mutex) - if (lock.compareAndSet(state, nestedFatLock)) return - } else if (state.ownerThreadId != null) { - val fatLock = - LockState(FAT, state.nestedLocks, state.waiters + 1, state.ownerThreadId, state.mutex) - if (lock.compareAndSet(state, fatLock)) { - fatLock.mutex!!.lock() - tryLockAfterResume(currentThreadId) - return - } - } - } - } - } - } - - public fun tryLock(): Boolean { - val currentThreadId = pthread_self()!! - while (true) { - val state = lock.value - if (state.status == UNLOCKED) { - val thinLock = LockState(THIN, 1, 0, currentThreadId) - if (lock.compareAndSet(state, thinLock)) - return true - } else { - if (currentThreadId == state.ownerThreadId) { - val nestedLock = - LockState(state.status, state.nestedLocks + 1, state.waiters, currentThreadId, state.mutex) - if (lock.compareAndSet(state, nestedLock)) - return true - } else { - return false - } - } - } - } - - public fun unlock() { - val currentThreadId = pthread_self()!! - while (true) { - val state = lock.value - require(currentThreadId == state.ownerThreadId) { "Thin lock may be only released by the owner thread, expected: ${state.ownerThreadId}, real: $currentThreadId" } - when (state.status) { - THIN -> { - // nested unlock - if (state.nestedLocks == 1) { - val unlocked = LockState(UNLOCKED, 0, 0) - if (lock.compareAndSet(state, unlocked)) - return - } else { - val releasedNestedLock = - LockState(THIN, state.nestedLocks - 1, state.waiters, state.ownerThreadId) - if (lock.compareAndSet(state, releasedNestedLock)) - return - } - } - - FAT -> { - if (state.nestedLocks == 1) { - // last nested unlock -> release completely, resume some waiter - val releasedLock = LockState(FAT, 0, state.waiters - 1, null, state.mutex) - if (lock.compareAndSet(state, releasedLock)) { - releasedLock.mutex!!.unlock() - return - } - } else { - // lock is still owned by the current thread - val releasedLock = - LockState(FAT, state.nestedLocks - 1, state.waiters, state.ownerThreadId, state.mutex) - if (lock.compareAndSet(state, releasedLock)) - return - } - } - - else -> error("It is not possible to unlock the mutex that is not obtained") - } - } - } - - private fun tryLockAfterResume(threadId: pthread_t) { - while (true) { - val state = lock.value - val newState = if (state.waiters == 0) // deflate - LockState(THIN, 1, 0, threadId) - else - LockState(FAT, 1, state.waiters, threadId, state.mutex) - if (lock.compareAndSet(state, newState)) { - if (state.waiters == 0) { - state.mutex!!.unlock() - mutexPool.release(state.mutex) - } - return - } - } - } - - protected class LockState( - val status: Status, - val nestedLocks: Int, - val waiters: Int, - val ownerThreadId: pthread_t? = null, - val mutex: NativeMutexNode? = null - ) - - protected enum class Status { UNLOCKED, THIN, FAT } + + private val nativeMutex = Mutex() + public fun lock() = nativeMutex.lock() + public fun tryLock(): Boolean = nativeMutex.tryLock() + public fun unlock() = nativeMutex.unlock() } public actual fun reentrantLock() = ReentrantLock() @@ -176,47 +28,4 @@ public actual inline fun synchronized(lock: SynchronizedObject, block: () -> } finally { lock.unlock() } -} - -private const val INITIAL_POOL_CAPACITY = 64 - -private val mutexPool by lazy { MutexPool(INITIAL_POOL_CAPACITY) } - -class MutexPool(capacity: Int) { - private val top = AtomicReference(null) - - private val mutexes = Array(capacity) { NativeMutexNode() } - - init { - // Immediately form a stack - for (mutex in mutexes) { - release(mutex) - } - } - - private fun allocMutexNode() = NativeMutexNode() - - fun allocate(): NativeMutexNode = pop() ?: allocMutexNode() - - fun release(mutexNode: NativeMutexNode) { - while (true) { - val oldTop = top.value - mutexNode.next = oldTop - if (top.compareAndSet(oldTop, mutexNode)) { - return - } - } - } - - private fun pop(): NativeMutexNode? { - while (true) { - val oldTop = top.value - if (oldTop == null) - return null - val newHead = oldTop.next - if (top.compareAndSet(oldTop, newHead)) { - return oldTop - } - } - } -} +} \ No newline at end of file diff --git a/atomicfu/src/nativeUnixLikeMain/kotlin/kotlinx/atomicfu/locks/NativeMutexNode.kt b/atomicfu/src/nativeUnixLikeMain/kotlin/kotlinx/atomicfu/locks/NativeMutexNode.kt deleted file mode 100644 index 032e6768..00000000 --- a/atomicfu/src/nativeUnixLikeMain/kotlin/kotlinx/atomicfu/locks/NativeMutexNode.kt +++ /dev/null @@ -1,31 +0,0 @@ -package kotlinx.atomicfu.locks - -import kotlinx.cinterop.* -import platform.posix.* -import kotlin.concurrent.* - -public actual class NativeMutexNode { - - @Volatile - private var isLocked = false - private val pMutex = nativeHeap.alloc().apply { pthread_mutex_init(ptr, null) } - private val pCond = nativeHeap.alloc().apply { pthread_cond_init(ptr, null) } - - internal actual var next: NativeMutexNode? = null - - actual fun lock() { - pthread_mutex_lock(pMutex.ptr) - while (isLocked) { // wait till locked are available - pthread_cond_wait(pCond.ptr, pMutex.ptr) - } - isLocked = true - pthread_mutex_unlock(pMutex.ptr) - } - - actual fun unlock() { - pthread_mutex_lock(pMutex.ptr) - isLocked = false - pthread_cond_broadcast(pCond.ptr) - pthread_mutex_unlock(pMutex.ptr) - } -} \ No newline at end of file