diff --git a/mirai-core-utils/build.gradle.kts b/mirai-core-utils/build.gradle.kts index 89a54cd25..2f7ee3652 100644 --- a/mirai-core-utils/build.gradle.kts +++ b/mirai-core-utils/build.gradle.kts @@ -15,7 +15,7 @@ plugins { kotlin("multiplatform") kotlin("plugin.serialization") - //id("kotlinx-atomicfu") + id("kotlinx-atomicfu") id("net.mamoe.kotlin-jvm-blocking-bridge") `maven-publish` id("com.jfrog.bintray") diff --git a/mirai-core-utils/src/commonMain/kotlin/AtomicLazy.kt b/mirai-core-utils/src/commonMain/kotlin/AtomicLazy.kt new file mode 100644 index 000000000..b44c6c2bd --- /dev/null +++ b/mirai-core-utils/src/commonMain/kotlin/AtomicLazy.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.utils + +import kotlinx.atomicfu.atomic +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +private val UNINITIALIZED: Any? = Symbol("UNINITIALIZED") + +/** + * - [initializer] is supported to be called at most once, however multiple invocations may happen if executed by multiple coroutines in single thread. + * - [ReadWriteProperty.setValue] prevails on competition with [initializer]. + */ +public fun lateinitMutableProperty(initializer: () -> T): ReadWriteProperty = + LateinitMutableProperty(initializer) + +private class LateinitMutableProperty( + initializer: () -> T +) : ReadWriteProperty { + private val value = atomic(UNINITIALIZED) + + private var initializer: (() -> T)? = initializer + + @Suppress("UNCHECKED_CAST") + override fun getValue(thisRef: Any?, property: KProperty<*>): T { + return when (val v = this.value.value) { + UNINITIALIZED -> synchronized(this) { + val initializer = initializer + if (initializer != null && this.value.value === UNINITIALIZED) { + val value = initializer() + this.value.compareAndSet(UNINITIALIZED, value) // setValue prevails + this.initializer = null + value + } else v as T + } + else -> v as T + } + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + this.value.value = value + } +} \ No newline at end of file diff --git a/mirai-core-utils/src/commonMain/kotlin/CoroutineUtils.kt b/mirai-core-utils/src/commonMain/kotlin/CoroutineUtils.kt index 2bffdcf5c..bfb0c542f 100644 --- a/mirai-core-utils/src/commonMain/kotlin/CoroutineUtils.kt +++ b/mirai-core-utils/src/commonMain/kotlin/CoroutineUtils.kt @@ -41,4 +41,24 @@ public inline fun CoroutineScope.launchWithPermit( return launch(coroutineContext) { semaphore.withPermit { block() } } +} + +/** + * Creates a child scope of the receiver scope. + */ +public fun CoroutineScope.childScope( + coroutineContext: CoroutineContext = EmptyCoroutineContext, +): CoroutineScope { + val ctx = this.coroutineContext + coroutineContext + return CoroutineScope(ctx + SupervisorJob(ctx.job)) +} + +/** + * Creates a child scope of the receiver context scope. + */ +public fun CoroutineContext.childScope( + coroutineContext: CoroutineContext = EmptyCoroutineContext, +): CoroutineScope { + val ctx = this + coroutineContext + return CoroutineScope(ctx + SupervisorJob(ctx.job)) } \ No newline at end of file diff --git a/mirai-core-utils/src/commonMain/kotlin/Symbol.kt b/mirai-core-utils/src/commonMain/kotlin/Symbol.kt new file mode 100644 index 000000000..c71ead05d --- /dev/null +++ b/mirai-core-utils/src/commonMain/kotlin/Symbol.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.utils + +public class Symbol private constructor(name: String) { + private val str = "Symbol($name)" + override fun toString(): String = str + + public companion object { + @Suppress("RedundantNullableReturnType") + @JvmName("create") + public operator fun invoke(name: String): Any? = Symbol(name) // calls constructor + } +} \ No newline at end of file diff --git a/mirai-core-utils/src/commonTest/kotlin/net/mamoe/mirai/utils/LateinitMutablePropertyTest.kt b/mirai-core-utils/src/commonTest/kotlin/net/mamoe/mirai/utils/LateinitMutablePropertyTest.kt new file mode 100644 index 000000000..1fe8c92c1 --- /dev/null +++ b/mirai-core-utils/src/commonTest/kotlin/net/mamoe/mirai/utils/LateinitMutablePropertyTest.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.utils + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import java.util.concurrent.CompletableFuture +import java.util.concurrent.atomic.AtomicInteger +import kotlin.concurrent.thread +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertSame + +internal class LateinitMutablePropertyTest { + @Test + fun canInitialize() { + val value = Symbol("expected") + val prop by lateinitMutableProperty { value } + assertSame(value, prop) + } + + @Test + fun canOverride() { + val value = Symbol("expected") + val overrode = Symbol("override") + + var prop by lateinitMutableProperty { value } + prop = overrode + assertSame(overrode, prop) + } + + @Test + fun initializerCalledOnce() { + val value = Symbol("expected") + val counter = AtomicInteger(0) + + val prop by lateinitMutableProperty { + counter.incrementAndGet() + value + } + assertSame(value, prop) + assertSame(value, prop) + assertEquals(1, counter.get()) + } + + @Test + fun initializerCalledOnceConcurrent() = runBlocking { + val value = Symbol("expected") + val counter = AtomicInteger(0) + + val verySlowInitializer = CompletableFuture() + + + val prop by lateinitMutableProperty { + counter.incrementAndGet() + verySlowInitializer.join() // do not use coroutine: coroutines run in same thread so `synchronized` doesnt work. + value + } + + + val lock = CompletableDeferred() + repeat(10) { + launch { + lock.join() + @Suppress("UNUSED_EXPRESSION") + prop + } + } + lock.complete(Unit) // resume callers + + + verySlowInitializer.complete(Unit) + + assertSame(value, prop) + assertEquals(1, counter.get()) + } + + @Test + fun setValuePrevailsOnCompetitionWithInitializer() { + val verySlowInitializer = CompletableFuture() + val override = Symbol("override") + val initializer = Symbol("initializer") + + var prop by lateinitMutableProperty { + verySlowInitializer.join() + initializer + } + + thread { println(prop) } + prop = override + verySlowInitializer.complete(Unit) + + assertSame(override, prop) + } +} \ No newline at end of file