diff --git a/kotlin-native/backend.native/tests/runtime/workers/lazy1.kt b/kotlin-native/backend.native/tests/runtime/workers/lazy1.kt index f15cc8f6bb2..d22a174b7c0 100644 --- a/kotlin-native/backend.native/tests/runtime/workers/lazy1.kt +++ b/kotlin-native/backend.native/tests/runtime/workers/lazy1.kt @@ -8,53 +8,93 @@ package runtime.workers.lazy1 import kotlin.test.* import kotlin.native.concurrent.* -class Lazy { +private var y = 20 + +class Lazy(mode: LazyThreadSafetyMode) { val x = 17 - val self by lazy { this } - val recursion: Int by lazy { + val self by lazy(mode) { this } + val recursion: Int by lazy(mode) { if (x < 17) 42 else recursion } - val freezer: Int by lazy { + val finiteRecursion : Int by lazy(mode) { + if (y < 17) 42 else { + y -= 1 + finiteRecursion + 1 + } + } + val freezer: Int by lazy(mode) { freeze() 42 } - val thrower: String by lazy { + val thrower: String by lazy(mode) { if (x < 100) throw IllegalArgumentException() "FAIL" } } +private val checkedLazyModes = + if (Platform.memoryModel != MemoryModel.EXPERIMENTAL) + listOf(LazyThreadSafetyMode.PUBLICATION) + else + listOf(LazyThreadSafetyMode.SYNCHRONIZED, LazyThreadSafetyMode.PUBLICATION) + @Test fun runTest1() { - assertFailsWith { - println(Lazy().recursion) - } - assertFailsWith { - println(Lazy().freeze().recursion) + for (mode in checkedLazyModes) { + // We decided to synchonaize behaviour to be consistent with jvm version. + // Anyway, it's doesn't looks like well-defined case + if (Platform.memoryModel == MemoryModel.EXPERIMENTAL) { + val expected = if (mode == LazyThreadSafetyMode.SYNCHRONIZED) 46 else 42 + y = 20 + assertEquals(Lazy(mode).finiteRecursion, expected) + y = 20 + assertEquals(Lazy(mode).freeze().finiteRecursion, expected) + } else { + assertFailsWith { + println(Lazy(mode).recursion) + } + assertFailsWith { + println(Lazy(mode).freeze().recursion) + } + y = 20 + assertFailsWith { + println(Lazy(mode).finiteRecursion) + } + y = 20 + assertFailsWith { + println(Lazy(mode).freeze().finiteRecursion) + } + } } } @Test fun runTest2() { - var sum = 0 - for (i in 1 .. 100) { - val self = Lazy().freeze() - assertEquals(self, self.self) - sum += self.self.hashCode() + for (mode in checkedLazyModes) { + var sum = 0 + for (i in 1..100) { + val self = Lazy(mode).freeze() + assertEquals(self, self.self) + sum += self.self.hashCode() + } } println("OK") } @Test fun runTest3() { - assertFailsWith { - println(Lazy().freezer) + for (mode in checkedLazyModes) { + assertFailsWith { + println(Lazy(mode).freezer) + } } } @Test fun runTest4() { - val self = Lazy() - repeat(10) { - assertFailsWith { - println(self.thrower) + for (mode in checkedLazyModes) { + val self = Lazy(mode) + repeat(10) { + assertFailsWith { + println(self.thrower) + } } } } \ No newline at end of file diff --git a/kotlin-native/backend.native/tests/runtime/workers/lazy4.kt b/kotlin-native/backend.native/tests/runtime/workers/lazy4.kt index 3a565de67b1..7af8f3323a8 100644 --- a/kotlin-native/backend.native/tests/runtime/workers/lazy4.kt +++ b/kotlin-native/backend.native/tests/runtime/workers/lazy4.kt @@ -11,16 +11,20 @@ import kotlin.native.concurrent.* const val WORKERS_COUNT = 20 -class C(private val initializer: () -> Int) { - val data by lazy { initializer() } +class IntHolder(val value:Int) + +class C(mode: LazyThreadSafetyMode, private val initializer: () -> IntHolder) { + val data by lazy(mode) { initializer() } } -fun concurrentLazyAccess(freeze: Boolean) { +fun concurrentLazyAccess(freeze: Boolean, mode: LazyThreadSafetyMode) { + // in old mm PUBLICATION is in fact SYNCHRONIZED, while SYNCHRONIZED is not supported + val argumentMode = if (Platform.memoryModel == MemoryModel.EXPERIMENTAL) mode else LazyThreadSafetyMode.PUBLICATION val initializerCallCount = AtomicInt(0) - val c = C { + val c = C(argumentMode) { initializerCallCount.increment() - 42 + IntHolder(42) } if (freeze) { c.freeze() @@ -40,14 +44,18 @@ fun concurrentLazyAccess(freeze: Boolean) { while (inited.value < workers.size) {} canStart.value = 1 - futures.forEach { - assertEquals(42, it.result) + val results = futures.map { it.result } + results.forEach { + assertEquals(42, it.value) + assertSame(results[0], it) } workers.forEach { it.requestTermination().result } - assertEquals(1, initializerCallCount.value) + if (mode == LazyThreadSafetyMode.SYNCHRONIZED) { + assertEquals(1, initializerCallCount.value) + } } @Test @@ -55,11 +63,13 @@ fun concurrentLazyAccessUnfrozen() { if (Platform.memoryModel != MemoryModel.EXPERIMENTAL) { return } - concurrentLazyAccess(false) + concurrentLazyAccess(false, LazyThreadSafetyMode.SYNCHRONIZED) + concurrentLazyAccess(false, LazyThreadSafetyMode.PUBLICATION) } @Test fun concurrentLazyAccessFrozen() { - concurrentLazyAccess(true) + concurrentLazyAccess(true, LazyThreadSafetyMode.SYNCHRONIZED) + concurrentLazyAccess(true, LazyThreadSafetyMode.PUBLICATION) } diff --git a/kotlin-native/runtime/src/main/kotlin/kotlin/Lazy.kt b/kotlin-native/runtime/src/main/kotlin/kotlin/Lazy.kt index 0631116caa2..40687105adb 100644 --- a/kotlin-native/runtime/src/main/kotlin/kotlin/Lazy.kt +++ b/kotlin-native/runtime/src/main/kotlin/kotlin/Lazy.kt @@ -5,9 +5,10 @@ package kotlin -import kotlin.native.concurrent.FreezeAwareLazyImpl +import kotlin.native.concurrent.* import kotlin.native.internal.FixmeConcurrency import kotlin.reflect.KProperty +import kotlin.native.isExperimentalMM /** * Creates a new instance of the [Lazy] that uses the specified initialization function [initializer] @@ -18,7 +19,13 @@ import kotlin.reflect.KProperty * Note that the returned instance uses itself to synchronize on. Do not synchronize from external code on * the returned instance as it may cause accidental deadlock. Also this behavior can be changed in the future. */ -public actual fun lazy(initializer: () -> T): Lazy = FreezeAwareLazyImpl(initializer) +@OptIn(kotlin.ExperimentalStdlibApi::class) +public actual fun lazy(initializer: () -> T): Lazy = + if (isExperimentalMM()) + SynchronizedLazyImpl(initializer) + else + FreezeAwareLazyImpl(initializer) + /** * Creates a new instance of the [Lazy] that uses the specified initialization function [initializer] @@ -31,10 +38,11 @@ public actual fun lazy(initializer: () -> T): Lazy = FreezeAwareLazyImpl( * Also this behavior can be changed in the future. */ @FixmeConcurrency +@OptIn(kotlin.ExperimentalStdlibApi::class) public actual fun lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy = when (mode) { - LazyThreadSafetyMode.SYNCHRONIZED -> throw UnsupportedOperationException() - LazyThreadSafetyMode.PUBLICATION -> FreezeAwareLazyImpl(initializer) + LazyThreadSafetyMode.SYNCHRONIZED -> if (isExperimentalMM()) SynchronizedLazyImpl(initializer) else throw UnsupportedOperationException() + LazyThreadSafetyMode.PUBLICATION -> if (isExperimentalMM()) SafePublicationLazyImpl(initializer) else FreezeAwareLazyImpl(initializer) LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer) } diff --git a/kotlin-native/runtime/src/main/kotlin/kotlin/native/concurrent/Lazy.kt b/kotlin-native/runtime/src/main/kotlin/kotlin/native/concurrent/Lazy.kt index 0e814614f64..2aacb316b4a 100644 --- a/kotlin-native/runtime/src/main/kotlin/kotlin/native/concurrent/Lazy.kt +++ b/kotlin-native/runtime/src/main/kotlin/kotlin/native/concurrent/Lazy.kt @@ -121,3 +121,78 @@ internal class AtomicLazyImpl(initializer: () -> T) : Lazy { * such as object signletons, or in cases where it's guaranteed not to have cyclical garbage. */ public fun atomicLazy(initializer: () -> T): Lazy = AtomicLazyImpl(initializer) + +@Suppress("UNCHECKED_CAST") +internal class SynchronizedLazyImpl(initializer: () -> T) : Lazy { + private var initializer = FreezableAtomicReference<(() -> T)?>(initializer) + private var valueRef = FreezableAtomicReference(UNINITIALIZED) + private val lock = Lock() + + override val value: T + get() { + val _v1 = valueRef.value + if (_v1 !== UNINITIALIZED) { + return _v1 as T + } + + return locked(lock) { + val _v2 = valueRef.value + if (_v2 === UNINITIALIZED) { + val wasFrozen = this.isFrozen + val typedValue = initializer.value!!() + if (this.isFrozen) { + if (!wasFrozen) { + throw InvalidMutabilityException("Frozen during lazy computation") + } + typedValue.freeze() + } + valueRef.value = typedValue + initializer.value = null + typedValue + } else { + _v2 as T + } + } + } + + override fun isInitialized() = valueRef.value !== UNINITIALIZED + + override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet." +} + + +@Suppress("UNCHECKED_CAST") +internal class SafePublicationLazyImpl(initializer: () -> T) : Lazy { + private var initializer = FreezableAtomicReference<(() -> T)?>(initializer) + private var valueRef = FreezableAtomicReference(UNINITIALIZED) + + override val value: T + get() { + val value = valueRef.value + if (value !== UNINITIALIZED) { + return value as T + } + + val initializerValue = initializer.value + // if we see null in initializer here, it means that the value is already set by another thread + if (initializerValue != null) { + val wasFrozen = this.isFrozen + val newValue = initializerValue() + if (this.isFrozen) { + if (!wasFrozen) { + throw InvalidMutabilityException("Frozen during lazy computation") + } + newValue.freeze() + } + if (valueRef.compareAndSet(UNINITIALIZED, newValue)) { + initializer.value = null + return newValue + } + } + return valueRef.value as T + } + + override fun isInitialized(): Boolean = valueRef.value !== UNINITIALIZED + + override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet." +} \ No newline at end of file