mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2026-10-19 19:30:22 +00:00
one more rewrite of semaphore
This commit is contained in:
16
coroutines/benchmark/build.gradle
Normal file
16
coroutines/benchmark/build.gradle
Normal file
@@ -0,0 +1,16 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.jvm"
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api project(":micro_utils.coroutines")
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
56
coroutines/benchmark/src/main/kotlin/main.kt
Normal file
56
coroutines/benchmark/src/main/kotlin/main.kt
Normal file
@@ -0,0 +1,56 @@
|
||||
import dev.inmo.micro_utils.coroutines.SmartSemaphore
|
||||
import dev.inmo.micro_utils.coroutines.withAcquire
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import kotlin.math.tanh
|
||||
import kotlin.system.measureNanoTime
|
||||
|
||||
data class Record(
|
||||
val permits: Int,
|
||||
val i: Int,
|
||||
val timeNanos: Long
|
||||
)
|
||||
|
||||
private const val permitsCount = 1000
|
||||
private val acquiresCount = 10000
|
||||
|
||||
suspend fun main(args: Array<String>) {
|
||||
val outputFilePath = args.firstOrNull() ?: "local.bench.txt"
|
||||
|
||||
val records = ArrayList<Record>(permitsCount * acquiresCount)
|
||||
val jobs = ArrayList<Job>()
|
||||
val scope = CoroutineScope(Dispatchers.Default)
|
||||
for (permits in 1 .. permitsCount) {
|
||||
val semaphore = SmartSemaphore.Mutable(permits)
|
||||
for (i in 0 until acquiresCount) {
|
||||
jobs.add(
|
||||
scope.launch(start = CoroutineStart.LAZY) {
|
||||
val timeNanos = measureNanoTime {
|
||||
semaphore.withAcquire {
|
||||
tanh(57.179)
|
||||
}
|
||||
}
|
||||
records.add(Record(permits, i, timeNanos))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
jobs.joinAll()
|
||||
val file = File(outputFilePath)
|
||||
file.writeText("")
|
||||
val text = buildString {
|
||||
records.groupBy { it.permits }.forEach {
|
||||
it.value.sortedBy { it.i }.forEach {
|
||||
append("${it.permits} ${it.i} ${it.timeNanos}\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
file.appendText(text)
|
||||
file.appendText("\n")
|
||||
println(text)
|
||||
}
|
||||
@@ -61,6 +61,7 @@ sealed interface SmartSemaphore {
|
||||
override val permitsStateFlow: StateFlow<Int> = _freePermitsStateFlow.asStateFlow()
|
||||
|
||||
private val internalChangesMutex = Mutex(false)
|
||||
private val acquirePermitsMutex = Mutex(false)
|
||||
|
||||
fun immutable() = Immutable(permitsStateFlow, maxPermits)
|
||||
|
||||
@@ -70,28 +71,24 @@ sealed interface SmartSemaphore {
|
||||
* Holds call until this [SmartSemaphore] will be re-locked. That means that current method will
|
||||
*/
|
||||
suspend fun acquire(permits: Int = 1) {
|
||||
var acquiredPermits = 0
|
||||
val checkedPermits = checkedPermits(permits)
|
||||
try {
|
||||
do {
|
||||
val shouldContinue = internalChangesMutex.withLock {
|
||||
val requiredPermits = checkedPermits - acquiredPermits
|
||||
val acquiring = minOf(freePermits, requiredPermits)
|
||||
if (acquiring <= 0) return@withLock true
|
||||
acquiredPermits += acquiring
|
||||
_freePermitsStateFlow.value -= acquiring
|
||||
|
||||
acquiredPermits != checkedPermits
|
||||
}
|
||||
if (shouldContinue) {
|
||||
waitRelease(checkedPermits - acquiredPermits)
|
||||
}
|
||||
} while (shouldContinue && currentCoroutineContext().isActive)
|
||||
} catch (e: Throwable) {
|
||||
if (acquiredPermits > 0) {
|
||||
release(acquiredPermits)
|
||||
var changed = false
|
||||
acquirePermitsMutex.withLock {
|
||||
_freePermitsStateFlow.first {
|
||||
it >= checkedPermits
|
||||
}
|
||||
internalChangesMutex.withLock {
|
||||
try {
|
||||
_freePermitsStateFlow.value -= checkedPermits
|
||||
changed = true
|
||||
suspendPoint()
|
||||
} catch (e: Throwable) {
|
||||
if (changed) {
|
||||
_freePermitsStateFlow.value += checkedPermits
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,19 +96,9 @@ sealed interface SmartSemaphore {
|
||||
* Holds call until this [SmartSemaphore] will be re-locked. That means that while [freePermits] == true, [holds] will
|
||||
* wait for [freePermits] == false and then try to lock
|
||||
*/
|
||||
@Deprecated("Use acquire instead", ReplaceWith("acquire(permits)"))
|
||||
suspend fun acquireByOne(permits: Int = 1) {
|
||||
val checkedPermits = checkedPermits(permits)
|
||||
do {
|
||||
waitRelease(checkedPermits)
|
||||
val shouldContinue = internalChangesMutex.withLock {
|
||||
if (_freePermitsStateFlow.value < checkedPermits) {
|
||||
true
|
||||
} else {
|
||||
_freePermitsStateFlow.value -= checkedPermits
|
||||
false
|
||||
}
|
||||
}
|
||||
} while (shouldContinue && currentCoroutineContext().isActive)
|
||||
return acquire(permits)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,18 +107,29 @@ sealed interface SmartSemaphore {
|
||||
* @return True if lock was successful. False otherwise
|
||||
*/
|
||||
suspend fun tryAcquire(permits: Int = 1): Boolean {
|
||||
if (acquirePermitsMutex.tryLock() == false) return false
|
||||
|
||||
val checkedPermits = checkedPermits(permits)
|
||||
return if (_freePermitsStateFlow.value >= checkedPermits) {
|
||||
internalChangesMutex.withLock {
|
||||
var changed = false
|
||||
|
||||
return internalChangesMutex.withLock {
|
||||
try {
|
||||
if (_freePermitsStateFlow.value >= checkedPermits) {
|
||||
_freePermitsStateFlow.value -= checkedPermits
|
||||
changed = true
|
||||
suspendPoint()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (changed) {
|
||||
_freePermitsStateFlow.value += checkedPermits
|
||||
}
|
||||
throw e
|
||||
} finally {
|
||||
acquirePermitsMutex.unlock()
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import dev.inmo.micro_utils.coroutines.SmartSemaphore
|
||||
import dev.inmo.micro_utils.coroutines.withAcquire
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.flow.first
|
||||
@@ -38,35 +39,6 @@ class SmartSemaphoreTests {
|
||||
assertEquals(2, semaphore.freePermits)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cancelledAcquireReturnsPartialPermitsUnderContention() = runTest(timeout = 5.seconds) {
|
||||
// One permit belongs to another holder; the waiter can acquire two
|
||||
// permits immediately, but must wait for the third.
|
||||
val semaphore = SmartSemaphore.Mutable(permits = 3, acquiredPermits = 1)
|
||||
lateinit var waiter: kotlinx.coroutines.Job
|
||||
val cancellation = launch(Dispatchers.Unconfined) {
|
||||
semaphore.permitsStateFlow.first { it == 1 }
|
||||
waiter.cancel()
|
||||
}
|
||||
waiter = launch(Dispatchers.Unconfined) {
|
||||
semaphore.acquire(3)
|
||||
}
|
||||
assertEquals(0, semaphore.freePermits)
|
||||
assertFalse(waiter.isCompleted)
|
||||
|
||||
// Publishing this release resumes the observer while the internal
|
||||
// mutex is held. The cancelled acquire must wait to roll back safely.
|
||||
semaphore.release()
|
||||
cancellation.join()
|
||||
waiter.join()
|
||||
assertTrue(waiter.isCancelled)
|
||||
assertEquals(3, semaphore.freePermits, "Cancellation must return both partially acquired permits")
|
||||
semaphore.withAcquire(3) {
|
||||
assertEquals(0, semaphore.freePermits)
|
||||
}
|
||||
assertEquals(3, semaphore.freePermits)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cancelledAcquireWithoutPermitsDoesNotReleaseAnotherHoldersPermit() = runTest(timeout = 5.seconds) {
|
||||
val semaphore = SmartSemaphore.Mutable(permits = 1, acquiredPermits = 1)
|
||||
|
||||
@@ -35,6 +35,7 @@ String[] includes = [
|
||||
":ktor:client",
|
||||
":coroutines",
|
||||
":coroutines:compose",
|
||||
":coroutines:benchmark",
|
||||
":android:recyclerview",
|
||||
":android:pickers",
|
||||
":android:smalltextfield",
|
||||
|
||||
Reference in New Issue
Block a user